Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions net/lease-sync/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (c) 2025-2026 Pierre Gaufillet <pierre.gaufillet@bergamote.eu>

include $(TOPDIR)/rules.mk

PKG_NAME:=lease-sync
PKG_VERSION:=1.1.0
PKG_RELEASE:=1

PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/pgaufillet/lease-sync.git
PKG_SOURCE_VERSION:=v1.1.0
PKG_MIRROR_HASH:=3fcf2cb2eeb20ba5171c35a8d431db752a645dc50f9474f6f803e0ada5a2d28e

PKG_MAINTAINER:=Pierre Gaufillet <pierre.gaufillet@bergamote.eu>
PKG_LICENSE:=GPL-2.0-or-later
PKG_LICENSE_FILES:=LICENSE

PKG_BUILD_PARALLEL:=1

include $(INCLUDE_DIR)/package.mk

define Package/lease-sync
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
TITLE:=DHCP Lease Synchronization Daemon
URL:=https://github.com/pgaufillet/lease-sync
DEPENDS:=+libubus +libubox +libopenssl +dnsmasq
endef

define Package/lease-sync/description
High-availability DHCP lease synchronization daemon for dnsmasq.
Enables active/active HA clusters by synchronizing DHCP leases
between multiple dnsmasq instances via ubus and UDP.
endef

define Package/lease-sync/conffiles
/etc/config/lease-sync
/etc/lease-sync/
endef

define Build/Compile
$(MAKE) -C $(PKG_BUILD_DIR)/daemon \
CC="$(TARGET_CC)" \
CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS)" \
LDFLAGS="$(TARGET_LDFLAGS)" \
LIBS="-lubus -lubox -lssl -lcrypto" \
STATEDIR="/etc/lease-sync"
endef

define Package/lease-sync/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/daemon/lease-sync $(1)/usr/sbin/

$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/lease-sync.init $(1)/etc/init.d/lease-sync

$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/lease-sync.config $(1)/etc/config/lease-sync

$(INSTALL_DIR) $(1)/etc/lease-sync

$(INSTALL_DIR) $(1)/etc/hotplug.d/dhcp
$(INSTALL_BIN) ./files/lease-sync.hotplug $(1)/etc/hotplug.d/dhcp/50-lease-sync

endef

define Package/lease-sync/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/lease-sync stop >/dev/null 2>&1
/etc/init.d/lease-sync disable >/dev/null 2>&1
fi
exit 0
endef

define Package/lease-sync/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/lease-sync enable
echo "NOTE: Restart dnsmasq to activate the DHCP hotplug handler:"
echo " /etc/init.d/dnsmasq restart"
}
exit 0
endef

$(eval $(call BuildPackage,lease-sync))
12 changes: 12 additions & 0 deletions net/lease-sync/files/lease-sync.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
config lease-sync 'config'
option enabled '0'
option node_id ''
option sync_port '5378'
option sync_interval '30'
option peer_timeout '120'
option persist_interval '60'
option log_level '2'

# Example peer configuration
# config peer
# option address '192.168.1.2'
112 changes: 112 additions & 0 deletions net/lease-sync/files/lease-sync.hotplug
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (c) 2025-2026 Pierre Gaufillet <pierre.gaufillet@bergamote.eu>
#
# DHCP Lease Event Handler for lease-sync
# This hotplug script publishes DHCP events to ubus for the lease-sync daemon
# Installed to: /etc/hotplug.d/dhcp/50-lease-sync

# Only process DHCP lease events
[ -n "$ACTION" ] || exit 0

# Configuration
UBUS_EVENT="dhcp.lease"
LOG_TAG="lease-sync-hotplug"
NODE_ID_FILE="/etc/lease-sync/node-id"

# Logging function
log() {
logger -t "$LOG_TAG" -p "daemon.$1" "$2"
}

# Get or generate node ID
get_node_id() {
if [ -f "$NODE_ID_FILE" ]; then
cat "$NODE_ID_FILE"
else
cat /sys/class/net/br-lan/address 2>/dev/null || hostname
fi
}

# Sanitize a string for safe JSON embedding.
# Escapes backslash and double-quote characters to prevent injection.
sanitize_json() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# Build JSON payload for ubus event
build_lease_json() {
local json='{'

# Map hotplug ACTION to lease-sync action
case "$ACTION" in
add) json="${json}\"action\":\"add\"" ;;
remove) json="${json}\"action\":\"del\"" ;;
update) json="${json}\"action\":\"old\"" ;;
*) return 1 ;;
esac

# Required fields from hotplug environment
# (HOSTNAME is untrusted DHCP client input — must sanitize)
[ -n "$IPADDR" ] && json="${json},\"ip\":\"$IPADDR\""
[ -n "$MACADDR" ] && json="${json},\"mac\":\"$MACADDR\""
[ -n "$HOSTNAME" ] && json="${json},\"hostname\":\"$(sanitize_json "$HOSTNAME")\""

# Node identification
json="${json},\"node_id\":\"$(sanitize_json "$(get_node_id)")\""
json="${json},\"timestamp\":$(date +%s)"

# Lease timing (from dnsmasq environment, validated as numeric)
case "$DNSMASQ_LEASE_EXPIRES" in
''|*[!0-9]*) ;;
*) json="${json},\"expires\":$DNSMASQ_LEASE_EXPIRES" ;;
esac
case "$DNSMASQ_TIME_REMAINING" in
''|*[!0-9]*) ;;
*) json="${json},\"time_remaining\":$DNSMASQ_TIME_REMAINING" ;;
esac
case "$DNSMASQ_LEASE_LENGTH" in
''|*[!0-9]*) ;;
*) json="${json},\"lease_length\":$DNSMASQ_LEASE_LENGTH" ;;
esac

# Client identification
[ -n "$DNSMASQ_CLIENT_ID" ] && json="${json},\"client_id\":\"$DNSMASQ_CLIENT_ID\""
[ -n "$DNSMASQ_INTERFACE" ] && json="${json},\"interface\":\"$DNSMASQ_INTERFACE\""

# IPv6 specific fields (IAID may have "T" prefix for temporary addresses)
if [ -n "$DNSMASQ_IAID" ]; then
local iaid_num
iaid_num=$(echo "$DNSMASQ_IAID" | sed 's/^T//')
case "$iaid_num" in
''|*[!0-9]*) log err "Invalid DNSMASQ_IAID='$DNSMASQ_IAID', skipping" ;;
*)
json="${json},\"iaid\":$iaid_num"
if echo "$DNSMASQ_IAID" | grep -q "^T"; then
json="${json},\"is_temporary\":1"
else
json="${json},\"is_temporary\":0"
fi
;;
esac
fi

json="${json}}"
echo "$json"
}

# Publish event to ubus
case "$ACTION" in
add|remove|update)
[ -z "$IPADDR" ] && exit 0

payload=$(build_lease_json)
if ubus send "$UBUS_EVENT" "$payload" 2>/dev/null; then
log info "$ACTION event for $IPADDR published"
else
log err "Failed to publish $ACTION event for $IPADDR"
fi
;;
esac

exit 0
104 changes: 104 additions & 0 deletions net/lease-sync/files/lease-sync.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/bin/sh /etc/rc.common
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (c) 2025-2026 Pierre Gaufillet <pierre.gaufillet@bergamote.eu>
# lease-sync daemon init script for OpenWrt (procd)

START=99
STOP=10

USE_PROCD=1

PROG=/usr/sbin/lease-sync
CONF=/etc/lease-sync/config

start_service() {
# Ensure configuration directory exists
[ -d /etc/lease-sync ] || mkdir -p /etc/lease-sync
[ -d /var/lib/lease-sync ] || mkdir -p /var/lib/lease-sync

# Generate configuration from UCI
. /lib/functions.sh
config_load lease-sync

local enabled sync_port sync_interval persist_interval peer_timeout log_level node_id
config_get_bool enabled config enabled 0

# If disabled in UCI, don't start
[ "$enabled" -eq 0 ] && {
logger -t lease-sync -p daemon.info "lease-sync disabled in UCI configuration"
return 0
}

config_get sync_port config sync_port "5378"
config_get sync_interval config sync_interval "30"
config_get persist_interval config persist_interval "60"
config_get peer_timeout config peer_timeout "120"
config_get log_level config log_level "2"
config_get node_id config node_id ""

# Generate flat configuration file
logger -t lease-sync -p daemon.info "Generating configuration from UCI"

cat > "$CONF" <<EOF
# Auto-generated from /etc/config/lease-sync
# Do not edit - changes will be overwritten
# Edit /etc/config/lease-sync instead

sync_port=$sync_port
sync_interval=$sync_interval
persist_interval=$persist_interval
peer_timeout=$peer_timeout
log_level=$log_level
EOF

# Add node_id if configured
[ -n "$node_id" ] && echo "node_id=$node_id" >> "$CONF"

# Add peers from UCI (list option)
add_peer() {
echo "peer=$1" >> "$CONF"
}
config_list_foreach config peer add_peer

logger -t lease-sync -p daemon.info "Configuration generated with $(grep -c '^peer=' "$CONF" 2>/dev/null || echo 0) peers"

procd_open_instance lease-sync
procd_set_param command "$PROG" -c "$CONF"

# Respawn settings
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}

# File descriptor limits
procd_set_param limits nofile="1024 1024"

# Standard streams
procd_set_param stdout 1
procd_set_param stderr 1

# Use syslog for logging
procd_set_param syslog 1

procd_close_instance

logger -t lease-sync -p daemon.info "Starting lease-sync daemon"
}

stop_service() {
logger -t lease-sync -p daemon.info "Stopping lease-sync daemon"
}

service_triggers() {
# Reload on network changes
procd_add_reload_trigger "network"

# Watch configuration file for changes
procd_add_reload_trigger "lease-sync"
}

# lease-sync does not support SIGHUP config reload, so reload_service()
# performs a full restart (stop + start). Brief sync gap during restart.
reload_service() {
logger -t lease-sync -p daemon.info "Restarting lease-sync daemon (full restart, no SIGHUP support)"
stop
start
}
Loading