Skip to content

Commit 51642de

Browse files
committed
Add DHCP-style IP leases, store tunnel_ip on tree nodes, fix IP reclaim
IPAM: - Reclaim existing IP on reconnect (last_seen updated on each join) - Dynamic lease expiry: 7 days at <50% full, scaling to 0 at 100% Formula: lease_hours = 168/100 * (100 - max(0, (pct_full-50)*2)) - sweep_expired_tunnel_leases() for periodic cleanup - update_tunnel_last_seen() for heartbeat tracking Join handler: - Store tunnel_ip on endpoint tree node so it's visible in tree browser - Update wg_pubkey on existing nodes - Register private DNS for client: private.<id>.ep.<domain>
1 parent 2a3f505 commit 51642de

File tree

3 files changed

+106
-2
lines changed

3 files changed

+106
-2
lines changed

projects/LemonadeNexus/include/LemonadeNexus/IPAM/IPAMService.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ class IPAMService : public core::IService<IPAMService>,
6565
/// Collect server_node_ids whose backbone allocation is stale.
6666
[[nodiscard]] std::vector<std::string> collect_stale_backbone(uint64_t stale_threshold_sec) const;
6767

68+
// --- DHCP-style tunnel lease management ---
69+
70+
/// Compute lease timeout in seconds based on block fullness.
71+
/// Base: 7 days. Scales from 50% full (7 days) to 100% full (0).
72+
[[nodiscard]] uint64_t tunnel_lease_timeout_sec() const;
73+
74+
/// Sweep expired tunnel leases and release them. Returns count released.
75+
uint32_t sweep_expired_tunnel_leases();
76+
77+
/// Update last_seen for a tunnel client (call on heartbeat/join).
78+
void update_tunnel_last_seen(std::string_view node_id);
79+
6880
/// Set callback for allocation changes (gossip broadcast).
6981
using BackboneCallback = std::function<void(const BackboneAllocationDelta&)>;
7082
void set_backbone_callback(BackboneCallback cb) { backbone_callback_ = std::move(cb); }

projects/LemonadeNexus/src/Api/TreeApiHandler.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,19 @@ void TreeApiHandler::do_register_routes(httplib::Server& pub, httplib::Server& p
129129
ctx_.tree.insert_join_node(endpoint_node);
130130
}
131131

132+
// Allocate tunnel IP (returns existing if already allocated for this node)
132133
auto alloc = ctx_.ipam.allocate_tunnel_ip(node_id);
134+
135+
// Store the tunnel IP on the endpoint node so it's visible in the tree
136+
if (!alloc.base_network.empty()) {
137+
auto existing_node = ctx_.tree.get_node(node_id);
138+
if (existing_node && existing_node->tunnel_ip != alloc.base_network) {
139+
tree::TreeNode updated = *existing_node;
140+
updated.tunnel_ip = alloc.base_network;
141+
updated.wg_pubkey = body.value("wg_pubkey", existing_node->wg_pubkey);
142+
ctx_.tree.update_node_direct(node_id, updated);
143+
}
144+
}
133145
if (alloc.base_network.empty()) {
134146
error_response(res, "IP allocation failed", 409);
135147
return;

projects/LemonadeNexus/src/IPAM/IPAMService.cpp

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,12 @@ void IPAMService::on_stop() {
104104
Allocation IPAMService::do_allocate_tunnel_ip(std::string_view node_id) {
105105
std::lock_guard lock(mutex_);
106106

107-
// Check for existing tunnel allocation
107+
// Check for existing tunnel allocation — DHCP-style lease renewal
108108
auto it = allocations_.find(std::string(node_id));
109109
if (it != allocations_.end() && it->second.tunnel) {
110-
spdlog::warn("[{}] node '{}' already has a tunnel allocation", name(), node_id);
110+
it->second.tunnel->last_seen = now_unix();
111+
spdlog::info("[{}] node '{}' reclaimed tunnel {}", name(), node_id,
112+
it->second.tunnel->base_network);
111113
return *it->second.tunnel;
112114
}
113115

@@ -118,6 +120,7 @@ Allocation IPAMService::do_allocate_tunnel_ip(std::string_view node_id) {
118120
alloc.base_network = cidr;
119121
alloc.customer_node_id = std::string(node_id);
120122
alloc.allocated_at = now_unix();
123+
alloc.last_seen = now_unix();
121124

122125
allocations_[std::string(node_id)].tunnel = alloc;
123126
save_allocations();
@@ -475,6 +478,83 @@ bool IPAMService::cidrs_overlap(uint32_t a_ip, uint8_t a_prefix,
475478
return (a_ip & mask) == (b_ip & mask);
476479
}
477480

481+
// --- DHCP-style tunnel lease management ---
482+
483+
uint64_t IPAMService::tunnel_lease_timeout_sec() const {
484+
std::lock_guard lock(mutex_);
485+
486+
// Count tunnel allocations
487+
uint32_t tunnel_count = 0;
488+
for (const auto& [nid, aset] : allocations_) {
489+
if (aset.tunnel) ++tunnel_count;
490+
}
491+
492+
// Usable addresses: kTunnelSize - 10 (reserved .0-.9)
493+
constexpr uint32_t usable = kTunnelSize - 10;
494+
if (usable == 0) return 0;
495+
496+
// Percent full (0-100)
497+
double pct_full = (static_cast<double>(tunnel_count) / usable) * 100.0;
498+
499+
// Scale from 50%: below 50% = full 7 days, above 50% scales linearly to 0
500+
// Formula: lease_hours = 168 / 100 * (100 - PERCENT_FULL_SCALED_FROM_HALF)
501+
// where PERCENT_FULL_SCALED_FROM_HALF = max(0, (pct_full - 50) * 2)
502+
double scaled = std::max(0.0, (pct_full - 50.0) * 2.0);
503+
double lease_hours = 168.0 / 100.0 * (100.0 - scaled);
504+
lease_hours = std::max(0.0, lease_hours);
505+
506+
return static_cast<uint64_t>(lease_hours * 3600.0);
507+
}
508+
509+
uint32_t IPAMService::sweep_expired_tunnel_leases() {
510+
std::lock_guard lock(mutex_);
511+
512+
auto timeout = tunnel_lease_timeout_sec();
513+
if (timeout == 0) return 0;
514+
515+
auto now = now_unix();
516+
uint32_t released = 0;
517+
std::vector<std::string> to_release;
518+
519+
for (const auto& [nid, aset] : allocations_) {
520+
if (!aset.tunnel) continue;
521+
if (aset.tunnel->last_seen > 0 &&
522+
(now - aset.tunnel->last_seen) > timeout) {
523+
to_release.push_back(nid);
524+
}
525+
}
526+
527+
for (const auto& nid : to_release) {
528+
auto it = allocations_.find(nid);
529+
if (it == allocations_.end()) continue;
530+
531+
spdlog::info("[{}] lease expired for '{}' (tunnel {}), releasing",
532+
name(), nid, it->second.tunnel->base_network);
533+
it->second.tunnel.reset();
534+
535+
if (!it->second.tunnel && !it->second.private_subnet &&
536+
!it->second.shared_block && !it->second.backbone) {
537+
allocations_.erase(it);
538+
}
539+
++released;
540+
}
541+
542+
if (released > 0) {
543+
save_allocations();
544+
spdlog::info("[{}] swept {} expired tunnel leases (timeout={}h)",
545+
name(), released, timeout / 3600);
546+
}
547+
return released;
548+
}
549+
550+
void IPAMService::update_tunnel_last_seen(std::string_view node_id) {
551+
std::lock_guard lock(mutex_);
552+
auto it = allocations_.find(std::string(node_id));
553+
if (it != allocations_.end() && it->second.tunnel) {
554+
it->second.tunnel->last_seen = now_unix();
555+
}
556+
}
557+
478558
// --- Backbone (server mesh 172.16.0.0/22) ---
479559

480560
Allocation IPAMService::allocate_backbone_ip(std::string_view server_node_id,

0 commit comments

Comments
 (0)