From 367b72fd769fa2281d1d212b53da52d13a981a26 Mon Sep 17 00:00:00 2001 From: Philip Prindeville Date: Sat, 13 Sep 2025 11:34:48 -0600 Subject: [PATCH] kea: importer for legacy isc-dhcp-v4 UCI configs Now that ISC-DHCP is EOLs, users might want to transparently to the functionality of Kea. This supports most of the functionality of ISC-DHCP for v4. Signed-off-by: Philip Prindeville --- net/kea/Makefile | 24 +- net/kea/files/dhcp4.sh | 1014 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1034 insertions(+), 4 deletions(-) create mode 100755 net/kea/files/dhcp4.sh diff --git a/net/kea/Makefile b/net/kea/Makefile index 6689a632ed..b6238dd554 100644 --- a/net/kea/Makefile +++ b/net/kea/Makefile @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=kea PKG_VERSION:=3.0.2 -PKG_RELEASE:=5 +PKG_RELEASE:=6 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://ftp.isc.org/isc/kea/$(PKG_VERSION) @@ -42,6 +42,7 @@ Package/kea-ctrl/conffiles = $(Package/kea/conffiles) Package/kea-dhcp4/conffiles = $(Package/kea/conffiles) Package/kea-dhcp6/conffiles = $(Package/kea/conffiles) Package/kea-dhcp-ddns/conffiles = $(Package/kea/conffiles) +Package/kea-dhcp4-helper/conffiles = /etc/config/dhcp ###### ************************************************************************* define Package/kea @@ -73,7 +74,7 @@ define Package/kea-ctrl $(call Package/kea/Default) TITLE+=Control DEPENDS:=+procps-ng +procps-ng-ps +kea-dhcp4 \ - +IPV6:kea-dhcp6 +kea-dhcp-ddns + +IPV6:kea-dhcp6 +kea-dhcp-ddns endef define Package/kea-ctrl/description Tool to start, stop, reconfigure, and report status for the Kea servers. @@ -89,6 +90,17 @@ define Package/kea-dhcp4/description The DHCPv4 server process. This process responds to DHCPv4 queries from clients. endef +###### ************************************************************************* +define Package/kea-dhcp4-helper + $(call Package/kea/Default) + TITLE+=DHCP Server v4 importer of legacy UCI configs and Bind helper + DEPENDS:=@PACKAGE_bind-server +kea-dhcp4 +bind-rndc +bind-client +endef +define Package/kea-dhcp4-helper/description + The DHCPv4 UCI importer. This helper imports legacy UCI configs and then + primes local DNS zones in Bind. +endef + ###### ************************************************************************* define Package/kea-dhcp6 $(call Package/kea/Default) @@ -206,6 +218,11 @@ define Package/kea-dhcp4/install $(CP) $(PKG_INSTALL_DIR)/etc/kea/kea-dhcp4.conf $(1)/etc/kea/ endef +define Package/kea-dhcp4-helper/install + $(INSTALL_DIR) $(1)/usr/lib/kea/importers + $(INSTALL_BIN) ./files/dhcp4.sh $(1)/usr/lib/kea/importers/ +endef + define Package/kea-dhcp6/install $(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/kea $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/kea-dhcp6 $(1)/usr/sbin/kea-dhcp6 @@ -260,8 +277,6 @@ define Package/kea-uci/install $(INSTALL_DIR) $(1)/etc/config $(1)/etc/init.d $(INSTALL_CONF) ./files/kea.config $(1)/etc/config/kea $(INSTALL_BIN) ./files/kea.init $(1)/etc/init.d/kea - $(INSTALL_DIR) $(1)/usr/lib/kea/importers - $(INSTALL_BIN) ./files/dhcp4.sh $(1)/usr/lib/kea/importers/ endef define Package/kea-uci/conffiles @@ -272,6 +287,7 @@ $(eval $(call HostBuild)) $(eval $(call BuildPackage,kea-libs)) $(eval $(call BuildPackage,kea-ctrl)) $(eval $(call BuildPackage,kea-dhcp4)) +$(eval $(call BuildPackage,kea-dhcp4-helper)) $(eval $(call BuildPackage,kea-dhcp6)) $(eval $(call BuildPackage,kea-dhcp-ddns)) $(eval $(call BuildPackage,kea-admin)) diff --git a/net/kea/files/dhcp4.sh b/net/kea/files/dhcp4.sh new file mode 100755 index 0000000000..46735f240e --- /dev/null +++ b/net/kea/files/dhcp4.sh @@ -0,0 +1,1014 @@ +#!/bin/sh + +NL=$'\n' +WS=$'[\t ]' +TTL=3600 +PREFIX="update add" + +prog="$(basename $0)" + +dyndir=/var/run/dhcp +keadir=/var/lib/kea + +session_key_name=local-ddns + +getvar() { + local __dest="$1" _var="$2" + eval "export -n -- \"$__dest=\${$_var}\"" +} + +setvar() { + local __dest="$1" _val="$2" + eval "export -n -- \"$__dest=$_val\"" +} + +#### delete me -- these should all be in jshn.sh + +json_add_names() { + local _name + + for _name in "$@"; do + json_push_string "$_name" + done +} + +#### delete me + +time2seconds() { + local _var="$1" _timestring="$2" + local _multiplier _number _suffix + + _suffix="${_timestring//[0-9 ]}" + _number="${_timestring%%$_suffix}" + [ "$_number$_suffix" != "$_timestring" ] && return 1 + case "$_suffix" in + "" | s) + _multiplier=1 + ;; + m) + _multiplier=60 + ;; + h) + _multiplier=3600 + ;; + d) + _multiplier=86400 + ;; + w) + _multiplier=604800 + ;; + *) + return 1 + ;; + esac + + setvar "$_var" "$((_number * _multiplier))" +} + +explode_dotted() { + local _var="$1" _val="${2//\./ }" + + setvar "$_var" "$_val" +} + +is_decimal() { + local _val="$1" + + [ -z "${_val//[0-9]/}" ] +} + +is_hex() { + local _val="$1" + + [ -z "${_val//[0-9a-f]/}" ] +} + +trim() { + local _var="$1" _str="$2" _prev + + while true; do + _prev="$_str" + _str="${_str%%$WS}" + [ "$_str" = "$_prev" ] && break + done + while true; do + _prev="$_str" + _str="${_str##$WS}" + [ "$_str" = "$_prev" ] && break + done + + setvar "$_var" "$_str" +} + +mangle() { + local _var="$1" _name="${2//[^A-Za-z0-9]/_}" + + setvar "$_var" "$_name" +} + +rfc1918_prefix() { + local _var="$1" _subnet="${2%/*}" _exploded + explode_dotted _exploded "$_subnet" + set -- $_exploded + + case "$1.$2" in + 10.*) + setvar "$_var" "$1" ;; + 172.1[6789]|172.2[0-9]|172.3[01]|192.168) + setvar "$_var" "$1.$2" ;; + *) + setvar "$_var" "" ;; + esac +} + +no_ipv6() { + [ -n "$(named-checkconf -px \ + | sed -r -ne '1N; N; /^\tlisten-on-v6 ?\{\n\t\t"none";\n\t\};$/{ p; q; }; D')" ] +} + +subnet_of() { + local _var="$1" _ip + str2ip _ip "$2" || return 1 + local _ifname _pfx _start _end + + for _ifname in $dhcp_ifs; do + mangle _pfx "$_ifname" + + getvar _start "${_pfx}_start" + getvar _end "${_pfx}_end" + + if [ $_start -le $_ip ] && [ $_ip -le $_end ]; then + setvar "$_var" "$_ifname" + return 0 + fi + done + return 1 +} + +# duplicated from dnsmasq init script +hex_to_hostid() { + local _var="$1" + local _hex="${2#0x}" # strip optional "0x" prefix + + if ! is_hex "$_hex"; then + echo "Invalid hostid: $_hex" >&2 + return 1 + fi + + # convert into host id + setvar "$_var" "$( + printf "%0x:%0x" \ + $(((0x$_hex >> 16) % 65536)) \ + $(( 0x$_hex % 65536)) + )" + + return 0 +} + +update() { + local _lhs="$1" _family="$2" _type="$3" + shift 3 + + [ $dynamicdns -eq 1 ] && \ + echo -e "$PREFIX" "$_lhs $_family $_type $@\nsend" >> "$dyn_file" +} + +rev_str() { + local _var="$1" _str="$2" _delim="$3" + local _frag _result="" + + for _frag in ${_str//$_delim/ }; do + prepend _result "$_frag" "$_delim" + done + + setvar "$_var" "$_result" +} + +write_empty_zone() { + local zpath + zpath="$1" + + cat > "$zpath" <<\EOF +; +; BIND empty zone created by Kea dhcp4.sh plugin +; +$TTL 604800 +@ IN SOA localhost. root.localhost. ( + 1 ; Serial + 604800 ; Refresh + 86400 ; Retry + 419200 ; Expire + 604800 ) ; Negative Cache TTL +; +@ IN NS localhost. +EOF +} + +create_empty_zone() { + local zone error zpath command + zone="$1" + zpath="$dyndir/db.$zone" + + if [ ! -d "$dyndir" ]; then + mkdir -p "$dyndir" || return 1 + chown bind:bind "$dyndir" || return 1 + fi + + write_empty_zone "$zpath" + chown bind:bind "$zpath" || return 1 + chmod 0664 "$zpath" || return 1 + + # if the zone doesn't exist, or a RFC-1918 in-addr.arpa zone, then + # we need to add it, otherwise we need to modify it. + if ! rndc zonestatus $zone >/dev/null 2>&1; then + command="addzone" + else + command="modzone" + fi + + case "$zone" in + 10.in-addr.arpa|1[6789].172.in-addr.arpa|2[0-9].172.in-addr.arpa|3[01].172.in-addr.arpa|168.192.in-addr.arpa) + command="addzone" ;; + esac + + if ! error=$(rndc $command $zone "{ + type primary; + file \"$zpath\"; + update-policy { + grant $session_key_name zonesub any; + }; + };" 2>&1); then + case "$error" in + *"already exists"*) + ;; + *) + logger -s -p info -t "$prog" "Failed to add zone $zone: $error" + return 1 + ;; + esac + fi +} + +option_def() { + local name="$1" code="$2" type="$3" + + case "$type" in + binary|boolean|empty|fqdn|ipv4-address|ipv6-address|ipv6-prefix|psid|string|tuple|uint8|uint16|uint32|int8|int16|int32) + ;; + record) + echo "Not yet supported: $type" >&2 + exit 1 + ;; + *) + echo "Unknown option type: $type" >&2 + exit 1 + ;; + esac + + if ! json_get_type type "option-def"; then + json_add_array "option-def" + else + json_select "option-def" + fi + json_add_object + json_add_fields "name:string=$name" "code:int=$code" "type:string=$type" + json_close_object + + json_select ".." # option-def +} + +option_data() { + local arg value type + + # if the option-data array doesn't exist, create it since + # this is the first time through. otherwise, select it. + if ! json_get_type type "option-data"; then + json_add_array "option-data" + else + json_select "option-data" + fi + + json_add_object + + while [ $# -ge 1 ]; do + arg="$1" + shift + + case "$arg" in + name:*) + value="${arg#name:}" + json_add_string "name" "$value" + ;; + space:*) + value="${arg#space:}" + json_add_string "space" "$value" + ;; + code:*) + value="${arg#code:}" + if is_decimal "$value"; then + json_add_int "code" $value + else + echo "Bad code '$value' in DHCP options" >&2 + fi + ;; + csv-format:true) + json_add_boolean "csv-format" 1 + ;; + csv-format:false) + json_add_boolean "csv-format" 0 + ;; + data:*) + value="$arg" + json_add_fields "$value" + ;; + always-send:true) + json_add_boolean "always-send" 1 + ;; + *) + echo "Unexpected argument '$arg' to option_data" >&2 + ;; + esac + done + + json_close_object + + json_select .. # option-data +} + +is_force_send() { + local forced="$1" option="$2" + list_contains forced "$option" && echo "always-send:true" +} + +append_routes() { + local tuple + local network prefix router subnet + + trim tuple "$1" + + subnet="${tuple%%$WS*}" + + network="${subnet%/[0-9]*}" + + prefix="${subnet#*/}" + + router="${tuple#${subnet}$WS}" + + append routes "$subnet - $router" ", " +} + +append_dhcp_options() { + local tuple="$1" + + # strip redundant "option:" prefix + tuple="${tuple#option:}" + + local tag="${tuple%%,*}" + local values="${tuple#$tag,}" + + case "$tag" in + routers|time-servers|name-servers|domain-name-servers|log-servers|static-routes|ntp-servers|domain-search) + option_data "name:$tag" "data:string=$values" + ;; + dhcp-renewal-time) + if ! is_decimal "$values"; then + echo "Expected a decimal integer: $tag" >&2 + exit 1 + fi + ## option_data "name:$tag" "data:int=$values" + option_data "name:$tag" "data:string=$values" + ;; + *) + echo "Unhandled option: $tag" >&2 + ;; + esac +} + +static_cname_add() { + local cfg="$1" + local cname target + + config_get cname "$cfg" "cname" + [ -n "$cname" ] || return 0 + config_get target "$cfg" "target" + [ -n "$target" ] || return 0 + + case "$target" in + *.*) + ;; + *) + target="$target.$g_domain" + ;; + esac + + update "$cname.$g_domain." IN CNAME "$target." +} + +static_cnames() { + config_foreach static_cname_add cname "$@" +} + +static_domain_add() { + local cfg="$1" + local name ip ips revip octets + + config_get name "$cfg" "name" + [ -n "$name" ] || return 0 + config_get ip "$cfg" "ip" + [ -n "$ip" ] || return 0 + + ips="$ip" + for ip in $ips; do + rev_str revip "$ip" "." + + update "$name.$g_domain." IN A "$ip" + rfc1918_prefix octets "$ip" + [ -n "$octets" ] && \ + update "$revip.in-addr.arpa." IN PTR "$name.$g_domain." + done +} + +static_domains() { + config_foreach static_domain_add domain "$@" +} + +static_mxhost_add() { + local cfg="$1" + local h_domain relay pref + + config_get h_domain "$cfg" "domain" + [ -n "$h_domain" ] || return 0 + config_get relay "$cfg" "relay" + [ -n "$relay" ] || return 0 + config_get pref "$cfg" "pref" + [ -n "$pref" ] || return 0 + + case "$relay" in + *.*) + ;; + *) + relay="$relay.$g_domain" + ;; + esac + + if [ "$h_domain" = "@" ]; then + update "$g_domain." IN MX "$pref" "$relay." + else + update "$h_domain.$g_domain." IN MX "$pref" "$relay." + fi +} + +static_mxhosts() { + config_foreach static_mxhost_add mxhost "$@" +} + +static_srvhost_add() { + local cfg="$1" + local srv target port priority weight + + config_get srv "$cfg" "srv" + [ -n "$srv" ] || return 0 + config_get target "$cfg" "target" + [ -n "$target" ] || return 0 + config_get port "$cfg" "port" + [ -n "$port" ] || return 0 + config_get priority "$cfg" "priority" + [ -n "$priority" ] || return 0 + config_get weight "$cfg" "weight" + [ -n "$weight" ] || return 0 + + case "$target" in + *.*) + ;; + *) + target="$target.$g_domain" + ;; + esac + + update "$srv.$g_domain." IN SRV "$priority" "$weight" "$port" "$target." +} + +static_srvhosts() { + config_foreach static_srvhost_add srvhost "$@" +} + +static_host_add() { + local cfg="$1" + local broadcast hostid id macn macs mac name net ip ips revip leasetime + local h_domain s_domain defaultroute renewal_time s_renewal_time + local h_gateway s_gateway + local force_send always index + + config_get macs "$cfg" "mac" + [ -n "$macs" ] || return 0 + config_get name "$cfg" "name" + [ -n "$name" ] || return 0 + config_get ip "$cfg" "ip" + [ -n "$ip" ] || return 0 + + # needs to match a provisioned subnet + local ifname pfx + if ! subnet_of ifname "$ip"; then + echo "$name's address $ip doesn't match any subnet" >&2 + return 1 + fi + mangle pfx "$ifname" + getvar net "${pfx}_ifname" + getvar index "${net}_subnet4_index" + + local h_gateway s_gateway + getvar s_gateway "${pfx}_gateway" + + config_get_bool broadcast "$cfg" "broadcast" 0 + config_get dns "$cfg" "dns" + config_get h_gateway "$cfg" "gateway" "$s_gateway" + config_get leasetime "$cfg" "leasetime" + if [ -n "$leasetime" ]; then + time2seconds leasetime "$leasetime" || return 1 + fi + + config_get hostid "$cfg" "hostid" + if [ -n "$hostid" ]; then + hex_to_hostid hostid "$hostid" || return 1 + fi + + local s_defaultroute + getvar s_defaultroute "${pfx}_defaultroute" + + # if provisioned, otherwise default to subnet value + config_get_bool defaultroute "$cfg" "default_route" $s_defaultroute + + config_get force_send "$cfg" "force_send" + force_send="${force_send//,/ }" + + local s_domain + getvar s_domain "${pfx}_domain" + + config_get h_domain "$cfg" "domain" "$s_domain" + + getvar s_renewal_time "${pfx}_renewal_time" + + config_get renewal_time "$cfg" "renewal_time" "$s_renewal_time" + + json_select "$index" # why "$index" and not "$pfx"? + json_select "reservations" + + # rebinding-time) + + macn=0 + for mac in $macs; do + macn=$(( macn + 1 )) + done + + for mac in $macs; do + local secname="$name" + if [ $macn -gt 1 ]; then + secname="${name}-${mac//:}" + fi + + json_add_object "$mac" + + json_add_fields "hostname:string=$name" "hw-address:string=$mac" "ip-address:string=$ip" + + [ -n "$hostid" ] && json_add_fields "client-id:string=$hostid" + + ### redundant... + always="$(is_force_send "$force_send" "hostname")" + option_data "name:host-name" "data:string=$name" $always + + local routes= + config_list_foreach "$cfg" "routes" append_routes + + always="$(is_force_send "$force_send" "routes")" + if [ -n "$routes" -o -n "$always" ]; then + option_data "name:classless-static-route" "code:121" "data:string=$routes" $always + fi + + always="$(is_force_send "$force_send" "domain-name")" + if [ "$h_domain" != "$s_domain" -o -n "$always" ]; then + option_data "name:domain-name" "data:string=$h_domain" $always + fi + + always="$(is_force_send "$force_send" "fqdn")" + [ -n "$always" ] && option_data "name:host-name" "data:string=$name.$h_domain" $always + + if [ -n "$dns" ]; then + always="$(is_force_send "$force_send" "domain-name-servers")" + option_data "name:domain-name-servers" "data:string=$dns" $always + fi + + if [ "$h_gateway" != "$s_gateway" -a $defaultroute -eq 1 ]; then + always="$(is_force_send "$force_send" "routers")" + option_data "name:routers" "data:string=$h_gateway" $always + fi + + always="$(is_force_send "$force_send" "renewal-time")" + ## option_data "name:dhcp-renewal-time" "data:int=$renewal_time" $always + option_data "name:dhcp-renewal-time" "data:string=$renewal_time" $always + + ### need special handling for list dhcp_option 'option:xxx,yyy' + config_list_foreach "$cfg" "dhcp_option" append_dhcp_options + + # other options here + ### always-broadcast + ### default-lease-time + ### max-lease-time + + json_close_object # $mac + done + + json_select .. # reservations + json_select .. # $index + + ips="$ip" + for ip in $ips; do + rev_str revip "$ip" "." + + update "$name.$h_domain." IN A "$ip" + update "$revip.in-addr.arpa." IN PTR "$name.$h_domain." + done +} + +static_hosts() { + config_foreach static_host_add host "$@" +} + +gen_dhcp_subnet() { + local cfg="$1" index + + json_add_object "$cfg" + + json_get_index index + + subnet4_id=$((subnet4_id + 1)) + json_add_int "id" $subnet4_id + setvar "${cfg}_subnet4_id" "$subnet4_id" + + setvar "${cfg}_subnet4_index" "$index" + + json_add_fields "subnet:string=$NETWORK/$PREFIX" + + if [ -n "$START" ] && [ -n "$END" ]; then + json_add_array "pools" + json_add_object + json_add_fields "pool:string=$START - $END" + json_close_object + json_close_array # pools + fi + + if [ -n "$leasetime" ]; then + json_add_fields "valid-lifetime:int=$leasetime" "max-valid-lifetime:int=$leasetime" + fi + + option_data "name:subnet-mask" "data:string=$NETMASK" + + if [ -n "$BROADCAST" ] && [ "$BROADCAST" != "0.0.0.0" ]; then + option_data "name:broadcast-address" "data:string=$BROADCAST" + fi + + if [ $defaultroute -eq 1 ]; then + option_data "name:routers" "data:string=$gateway" + fi + + if [ -n "$DNS" ]; then + option_data "name:domain-name-servers" "data:string=$DNS" + fi + + if [ "$s_domain" != "$g_domain" ]; then + option_data "name:domain-name" "data:string=$s_domain" + fi + + [ -n "$ntp_servers" ] && option_data "name:ntp-servers" "data:string=$ntp_servers" + + [ -n "$routes" ] && option_data "name:classless-ipv4-route" "code:121" "csv-format:false" "data:string=$routes" + + if [ $dynamicdhcp -eq 0 ]; then + + if [ $authoritative -eq 1 ]; then + # see: + # https://gitlab.isc.org/isc-projects/kea/-/issues/4110 + # echo " deny unknown-clients;" + : + else + # echo " ignore unknown-clients;" + json_add_array "client-classes" + json_add_object + json_add_fields "name:string=DROP" "test:string=not(member('KNOWN'))" + json_close_object + json_close_array # client-classes + fi + fi + + config_list_foreach "$cfg" "dhcp_option" append_dhcp_options + + json_add_array "reservations" + json_close_array # reservations + + json_close_object # $cfg +} + +dhcpd_add() { + local cfg="$1" + local dhcp6range="::" + local dynamicdhcp defaultroute dnsserv dnsserver end + local gateway ifname ignore ntp_servers + local leasetime + local limit net netmask networkid octets pfx proto + local routes start subnet s_domain s_renewal_time + local IP NETMASK BROADCAST NETWORK PREFIX DNS START END + + config_get_bool ignore "$cfg" "ignore" 0 + + [ $ignore -eq 1 ] && return 0 + + config_get net "$cfg" "interface" + [ -n "$net" ] || return 0 + + config_get start "$cfg" "start" + config_get limit "$cfg" "limit" + + case "$start:$limit" in + :) + ;; + :*|*:) + echo "In pool $cfg start/limit must be used together" >&2 + return 0 + ;; + *:*) + # In Kea, this is done implicitly by not having a pool + # for unknown clients defined. + if [ $boot_unknown_clients -eq 1 ]; then + echo "To not boot unknown clients, remove the pool start and limit for $cfg" >&2 + fi + ;; + esac + + network_get_subnet subnet "$net" || return 0 + network_get_device ifname "$net" || return 0 + network_get_protocol proto "$net" || return 0 + + mangle pfx "$ifname" + + setvar "${pfx}_ifname" "$net" + + # only operate on statically provisioned interfaces + [ "$proto" != "static" ] && return 0 + + append dhcp_ifs "$ifname" + + rfc1918_prefix octets "$subnet" + + [ -n "$octets" ] && append rfc1918_nets "$octets" + + config_get_bool dynamicdhcp "$cfg" "dynamicdhcp" 1 + + config_get_bool defaultroute "$cfg" "default_route" 1 + setvar "${pfx}_defaultroute" $defaultroute + + ipcalc -d $subnet $start $limit + + setvar "${pfx}_start" "$NETWORK" + setvar "${pfx}_end" "$BROADCAST" + + ip2str IP "$IP" + ip2str NETMASK "$NETMASK" + ip2str NETWORK "$NETWORK" + ip2str BROADCAST "$BROADCAST" + [ -n "${START:+x}" ] && ip2str START "$START" + [ -n "${END:+x}" ] && ip2str END "$END" + + config_get netmask "$cfg" "netmask" "$NETMASK" + NETMASK="$netmask" + + config_get s_domain "$cfg" "domain" "$g_domain" + setvar "${pfx}_domain" "$s_domain" + + config_get ntp_servers "$cfg" "ntp_servers" "" + + config_get s_renewal_time "$cfg" "renewal_time" + if [ -n "$s_renewal_time" ]; then + time2seconds s_renewal_time "$s_renewal_time" || exit 1 + else + s_renewal_time="$g_renewal_time" + fi + setvar "${pfx}_renewal_time" "$s_renewal_time" + + config_get leasetime "$cfg" "leasetime" + if [ -n "$leasetime" ]; then + time2seconds leasetime "$leasetime" || return 1 + setvar "${pfx}_leasetime" "$leasetime" + fi + + if network_get_dnsserver dnsserver "$net" ; then + for dnsserv in $dnsserver; do + append DNS "$dnsserv" "," + done + else + DNS="$IP" + fi + + if ! network_get_gateway gateway "$net" ; then + gateway="$IP" + fi + setvar "${pfx}_gateway" $gateway + + routes= + config_list_foreach "$cfg" "routes" append_routes + + gen_dhcp_subnet "$cfg" +} + +general_config() { + local always_broadcast log_facility + local default_lease_time max_lease_time intf + + config_get_bool always_broadcast "isc_dhcpd" "always_broadcast" 0 + config_get_bool authoritative "isc_dhcpd" "authoritative" 1 + config_get_bool boot_unknown_clients "isc_dhcpd" "boot_unknown_clients" 1 + config_get default_lease_time "isc_dhcpd" "default_lease_time" 3600 + + config_get max_lease_time "isc_dhcpd" "max_lease_time" 86400 + + config_get g_renewal_time "isc_dhcpd" "renewal_time" + + config_get log_facility "isc_dhcpd" "log_facility" + + config_get g_domain "isc_dhcpd" "domain" + + config_get_bool dynamicdns "isc_dhcpd" dynamicdns 0 + + time2seconds default_lease_time "$default_lease_time" || return 1 + time2seconds max_lease_time "$max_lease_time" || return 1 + + if [ -n "$g_renewal_time" ]; then + time2seconds g_renewal_time "$g_renewal_time" || return 1 + else + g_renewal_time=$((default_lease_time / 2)) + fi + + setvar g_max_lease_time "$max_lease_time" + setvar g_lease_time "$default_lease_time" + setvar g_renewal_time "$g_renewal_time" + + json_add_object "lease-database" + json_add_string "type" "memfile" + json_add_boolean "persist" 1 + json_add_string "name" "$keadir/kea-leases4.csv" + json_add_int "lfc-interval" 900 + json_add_int "max-row-errors" 1 + json_close_object + + json_add_object "interfaces-config" + json_add_array "interfaces" + # will populate later + json_close_array # interfaces + + json_add_boolean "re-detect" 0 + json_add_string "dhcp-socket-type" "raw" + json_add_string "outbound-interface" "same-as-inbound" + json_close_object # interfaces-config + + ## option_def "renew-timer" 58 "uint32" + + [ $authoritative -eq 1 ] && json_add_boolean "authoritative" "1" + + json_add_boolean "ip-reservations-unique" "0" + + if [ $dynamicdns -eq 1 ]; then + json_add_fields "ddns-qualifying-suffix:string=$g_domain." "ddns-send-updates:boolean=1" + fi + + json_add_fields "valid-lifetime:int=$default_lease_time" "max-valid-lifetime:int=$g_max_lease_time" "renew-timer:int=$g_renewal_time" + + option_data "name:domain-name" "data:string=$g_domain" + + ### see: + ### https://gitlab.isc.org/isc-projects/kea/-/issues/241 + if [ $always_broadcast -eq 1 ]; then + echo "This option is deprecated and being ignored: always-broadcast" >&2 + fi +} + +write_zones() { + if [ $dynamicdns -eq 1 ]; then + rndc freeze + + create_empty_zone "$g_domain" + + local mynet + + for mynet in $rfc1918_nets; do + rev_str mynet "$mynet" "." + create_empty_zone "$mynet.in-addr.arpa" + done + + rndc thaw + fi + + rm -f /tmp/resolv.conf + echo "# This file is generated by the DHCPD service" > /tmp/resolv.conf + [ -n "$g_domain" ] && echo "domain $g_domain" >> /tmp/resolv.conf + echo "nameserver 127.0.0.1" >> /tmp/resolv.conf +} + +main() { + # values parsed by general_config that we need to persist + # for subsequent subnet and host configurations + local dhcp_ifs= dynamicdns authoritative boot_unknown_clients + local g_domain g_renewal_time g_max_lease_time g_lease_time + local rfc1918_nets="" + + local config_file="$1" + + if [ ! -f /etc/config/dhcp ]; then + return 0 + fi + + local dyn_file="$(mktemp -u /tmp/dhcpd.XXXXXX)" + + . /lib/functions.sh + . /lib/functions/ipv4.sh + . /lib/functions/network.sh + . /usr/share/libubox/jshn.sh + + mkdir -p "$keadir" + + config_load dhcp + + json_init + json_add_object "Dhcp4" + + general_config + + if [ $dynamicdns -eq 1 ]; then + cat < "$dyn_file" +; Generated by $prog at $(date) + +ttl $TTL + +EOF + fi + + local subnet4_id=0 + json_add_array "subnet4" + + config_foreach dhcpd_add dhcp + + static_hosts + + json_close_array # subnet4 + + json_add_array "host-reservation-identifiers" + json_add_names "hw-address" "client-id" + json_close_array # host-reservation-identifiers + + # json_add_string "reservation-mode" "global" + + json_add_boolean "reservations-in-subnet" 1 + + # plug the interfaces back in + json_select "interfaces-config" + json_select "interfaces" + json_add_names $dhcp_ifs + json_select .. + json_select .. + + json_close_object # Dhcp4 + + # the rest just generate DNS records + static_cnames + + static_domains + + static_mxhosts + + static_srvhosts + + write_zones + + # not running on any interfaces + [ -z "$dhcp_ifs" ] && return 1 + + rfc1918_nets="${rfc1918_nets// /$NL}" + rfc1918_nets="$(echo "$rfc1918_nets" | sort -V | uniq)" + rfc1918_nets="${rfc1918_nets//$NL/ }" + + if [ $dynamicdns -eq 1 ]; then + local args= + + no_ipv6 && args="-4" + + nsupdate -l -v $args "$dyn_file" + + fi + + rm -f "$dyn_file" + + json_pretty + json_dump | sed 's/\t/ /g' > "$config_file" + + return 0 +} + +main "$@" +