dhcpsd

The lua plugin loads /etc/dhcpsd/dhcp.lua so you can programatically define the address pools, static address mappings, customise the DHCP response based on the request and trigger actions based on the state of a comitted or expired lease.
Here is an example of how to use it.
-- Hostnames should be fully qualified.
-- If a domain isn't specified in the hostname then this will be appended.
local domain = 'internal'

-- Lookup table to match hostname to IP address.
-- Return a table of IP address and lease time to specify a lease time per host.
local hostnames = {
	['netbsd'] = '10.73.1.70',
	['freebsd'] = { '10.73.1.71', 30 },
}

-- Lookup table to match ethernet to hostnames.
local ethers = {
	['52:54:00:73:00:00'] = 'netbsd',
}

-- Lookup table to match hardware type.
local htypes = {
	[1] = ethers,
}

-- Lookup table for DHCP options, saves us using magic numbers below.
local dhcp_opts = {
	['SUBNETMASK'] = 1,
	['ROUTER'] = 3,
	['DNSSERVER'] = 6,
	['HOSTNAME'] = 12,
	['DNSDOMAIN'] = 15,
	['MTU'] = 26,
	['PARAMETERREQUESTLIST'] = 55,
	['FQDN'] = 81,
	['DOMAINSEARCH'] = 119,
}

-- Checks if the client requested the DHCP option or not.
local function has_parameter_request(opt)
	local oro = dhcp.get_option(dhcp_opts['PARAMETERREQUESTLIST'])
	if oro == nil then
		return false
	end
	if (string.find(oro, string.char(opt))) == nil then
		return false
	end
	return true
end

-- dhcpsd will call this function to make IP address pools for the interface.
-- You can return a single table with address, netmask, from and to
-- or a table of the above table (ie an array).
-- Each address MUST exist on the interface.
-- Setting a lease_time for the pool is optional.
function configure_pools(if_name)
	if if_name == 'bridge0' then
		return {
			address = '10.73.1.1',
			netmask = '255.255.255.0',
			from = '10.73.1.100',
			to = '10.73.1.200',
			lease_time = 3600,
		}
	end
end

local function add_domain(hostname)
	if hostname == nil or domain == nil or
		string.find(hostname, '%.') ~= nil
	then
		return hostname
	end
	return hostname .. '.' .. domain
end

local function ends_with_domain(hostname)
	if hostname == nil or domain == nil then
		return false
	end
	local d = '.' .. domain
	if string.sub(hostname, -string.len(d)) ~= d then
		return false
	end
	return true
end

local function trim_domain(hostname)
	if hostname ~= nil and ends_with_domain(hostname) then
		return string.sub(hostname, 1, string.len(domain) + 1)
	end
	return hostname
end

local function _lookup_hostname(htype, chaddr)
	local chaddrs = htypes[htype]
	if chaddrs ~= nil then
		return chaddrs[chaddr]
	end
end

-- dhcpsd will call this function to match a host to a hostname.
function lookup_hostname(htype, chaddr)
	local hname = _lookup_hostname(htype, chaddr)
	return add_domain(hname)
end

-- dhcpsd will call this function to match a host to an IP address.
function lookup_addr(hostname, htype, chaddr)
	local hname = _lookup_hostname(htype, chaddr)
	if hname ~= nil then
		hostname = hname
	end

	local addr = hostnames[hostname]
	if addr ~= nil then
		return addr
	end

	-- If the client has a fqdn hostname and we are being lazy by
	-- setting the domain at the top we need to trim it and look it up
	if hname == nil and hostname ~= nil and ends_with_domain(hostname) then
		hname = string.sub(hostname, 1, string.len(domain) + 2)
		return hostnames[hostname]
	end
end

-- dhcpsd will call this function to add options to a DHCP reply.
-- The dhcp table adds the following functions:
--     add_ip, add_string, add_uint32, add_uint16 and add_uint8
--     add_domain
--     set_bootp_file, set_bootp_sname
-- Return non zero to stop other plugins applying options.
function add_dhcp_options(hostname, htype, chaddr)
	if has_parameter_request(dhcp_opts['SUBNETMASK']) then
		dhcp.add_ip(dhcp_opts['SUBNETMASK'], '255.255.255.0')
	end
	if has_parameter_request(dhcp_opts['ROUTER']) then
		dhcp.add_ip(dhcp_opts['ROUTER'], '10.73.1.1')
	end
	if has_parameter_request(dhcp_opts['DNSSERVER']) then
		dhcp.add_ip(dhcp_opts['DNSSERVER'], '10.73.1.1, 10.73.1.2')
	end
	if has_parameter_request(dhcp_opts['DNSDOMAIN']) then
		dhcp.add_string(dhcp_opts['DNSDOMAIN'], domain)
	end
	if has_parameter_request(dhcp_opts['DOMAINSEARCH']) then
		dhcp.add_domain(dhcp_opts['DOMAINSEARCH'], domain)
	end

    -- If the subnet needs a specific MTU for PPPoE, etc
    -- dhcp.add_uint16(dhcp_opts['MTU'], 1480);

	hostname = trim_domain(hostname)
	if dhcp.get_option(dhcp_opts['HOSTNAME']) == 'netbsd' then
		dhcp.set_bootp_file('/boot-netbsd.img')
		dhcp.set_bootp_sname('tftp.local')
	elseif hostname == 'freebsd' then
		dhcp.set_bootp_file('/boot-freebsd.img')
		dhcp.set_bootp_sname('tftp.local')
		-- This is a buggy host so force this option in
		dhcp.add_string(dhcp_opts['DNSDOMAIN'], 'barfoo')
	else
		if has_parameter_request(dhcp_opts['DNSDOMAIN']) then
			dhcp.add_string(dhcp_opts['DNSDOMAIN'], 'foobar')
		end
	end
	return 1
end

local function arpa_ip(ip)
	local arpa = "in-addr.arpa."

	for a in string.gmatch(ip, "([^%.]+)") do
		arpa = a .. "." .. arpa
	end

	return arpa
end

--[[
-- example helper functions to update a DNS server using nsupdate(8).
-- You will need to install and configure nsupdate yourself,
-- or update your DNS server using some other means.
local function delete_dns(hostname, ip, flags)
	local arpa = arpa_ip(ip)

	if string.find(flags, "p") then
		os.execute("printf 'update delete " .. arpa .. " PTR\n"
			.. "send\n'"
			.. " | nsupdate")
	end

	if string.find(flags, "a") then
		os.execute("printf 'update delete " .. hostname .. ". A\n"
			.. "send\n'"
			.. " | nsupdate")
	end
end

local function update_dns(hostname, ip, flags, expires)
	-- We should checks the flags for more options:
	-- N means the client wants us to update the A record.
	-- P means the client wants us to update the PTR record.
	-- n means we have previously updated the A record.
	-- p means we have previously updated the PTR record.
	-- If the FQDN option is present and we don't have
	-- the P flag then then the client does NOT want us to
	-- update DNS at all.
	-- n and p can be returned to indicate what we have done
	local rflags = ""

	-- client did NOT tell us to NOT update DNS
	if string.find(flags, "P") ~= nil
		or dhcp.get_option(dhcp_opts['FQDN']) == nil
	then
		local ttl = string.format("%.0f", os.difftime(expires, os.time()))
		local arpa = arpa_ip(ip)
		local err = os.execute("printf 'update delete " .. arpa .. " PTR\n"
			.. "update add " .. arpa .. " " .. ttl .. " PTR " .. hostname .. ".\n"
			.. "send\n'"
			.. " | nsupdate")
		if err == true then
			rflags = rflags .. "p"
			if string.find(flags, "F") == nil or string.find(flags, "N") ~= nil then
				err = os.execute("printf 'update delete " .. hostname .. ". A\n"
					.. "update add " .. hostname .. ". " .. ttl .. " A " .. ip .. "\n"
					.. "send\n'"
					.. " | nsupdate")
				if err == true then
					rflags = rflags .. "n"
				end
			elseif string.find(flags, "n") then
				-- Unsure if this is the right thing to do as the
				-- client wants to update the PTR but not the A.
				delete_dns(hostname, ip, "n")
			end
		end
	end

	return rflags
end
--]]

---[[
-- dhcpsd will call this function lease time a lease is committed in some way.
-- dhcp.get_option can be called here to interogate the DHCP request from the
-- client, but you cannot set any options.
-- For example you could use this function to maintain entries in a DNS server.
function commit_lease(hostname, htype, chaddr, clientid, ip, flags, leased, expires)
	-- NOTE: dhcpsd MUST be supplied the debug flag to keep stdout open
	local rflags = ""
	local type = "STORED"
	if string.find(flags, "D") ~= nil or string.find(flags, "d") ~= nil then
		type = "DECLINED"
	elseif string.find(flags, "O") ~= nil then
		type = "OFFERED"
	elseif string.find(flags, "L") ~= nil then
		type = "LEASED"
	elseif string.find(flags, "I") ~= nil then
		type = "INFORMED"
	end

	io.write(string.format("%s: hostname:%s htype:%d chaddr:%s\n" ..
		"clientid:%s ip:%s flags:%s\n" ..
		"leased:%d (%s) expires:%d (%s)\n",
		type, hostname, htype, chaddr, clientid, ip, flags,
		leased, os.date("%c", leased), expires, os.date("%c", expires)))

	-- We could update DNS for a committed LEASE with an Address
	if type == "LEASED" and string.find(flags, "A") ~= nil
		and hostname ~= nil and hostname ~= ''
		and update_dns ~= nil
	then
		rflags = update_dns(hostname, ip, flags, expires)
	end

	return 0, rflags
end
--]]

---[[
-- dhcpcd will call this function when a lease has expired.
-- No dhcp functions can be called here.
-- You could use this function to maintain entries in a DNS server.
function expire_lease(hostname, clientid, ip, flags)
	-- NOTE: dhcpsd MUST be supplied the debug flag to keep stdout open
	io.write(string.format("EXPIRE: hostname:%s clientid: %s ip:%s flags:%s\n",
	hostname, clientid, ip, flags))

	if delete_dns ~= nil then
		delete_dns(hostname, ip, flags)
	end
	return 0
end
--]]