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
--]]