#!/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"

	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")"
		if [ -n "$renewal_time" ]; then
			option_data "name:dhcp-renewal-time" "data:string=$renewal_time" $always
		elif [ -n "$always" ]; then
			option_data "name:dhcp-renewal-time" "data:string=$s_renewal_time" $always
		fi

		### 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 <<EOF > "$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 "$@"

