@@ -104,10 +104,12 @@ void IPAMService::on_stop() {
104104Allocation 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
480560Allocation IPAMService::allocate_backbone_ip (std::string_view server_node_id,
0 commit comments