defmodule OneDHCPD.Server do
use GenServer
alias OneDHCPD.{Message, ARP, IPCalculator}
require Logger
@moduledoc """
This is the OneDHCPD Server.
Add it to a supervision tree in your application to use.
"""
@dhcp_server_port 67
@dhcp_client_port 68
@ip_broadcast {255, 255, 255, 255}
defmodule State do
@moduledoc false
defstruct [
:ifname,
:socket,
:subnet,
:subnet_mask,
:our_ip_address,
:their_ip_address
]
@type t :: %__MODULE__{
ifname: String.t(),
socket: any(),
subnet: :inet.ip4_address(),
subnet_mask: :inet.ip4_address(),
our_ip_address: :inet.ip4_address(),
their_ip_address: :inet.ip4_address()
}
end
@doc """
Return the server's name for the specified interface
"""
@spec server_name(String.t()) :: atom()
def server_name(ifname) do
Module.concat(__MODULE__, String.to_atom(ifname))
end
def child_spec([ifname, opts]) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [ifname, opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@doc """
Start a DHCP Server that works for one client.
Options:
* `port`: the port for the server (only specify if testing)
* `subnet`: a /30 subnet to allocate addresses (default is {192, 168, 200, 0})
"""
@spec start_link(String.t(), keyword()) :: GenServer.on_start()
def start_link(ifname, options) do
GenServer.start_link(__MODULE__, [{:ifname, ifname} | options], name: server_name(ifname))
end
def stop(ifname) do
GenServer.stop(server_name(ifname))
end
@spec init(keyword()) :: {:ok, OneDHCPD.Server.State.t()} | {:stop, atom()}
def init(options) do
ifname = Keyword.get(options, :ifname)
port = Keyword.get(options, :port, @dhcp_server_port)
subnet = Keyword.get(options, :subnet) || IPCalculator.default_subnet(ifname)
subnet_mask = IPCalculator.mask()
our_ip_address = IPCalculator.our_ip_address(subnet)
their_ip_address = IPCalculator.their_ip_address(subnet)
socket_opts = [
:binary,
{:bind_to_device, ifname},
{:broadcast, true},
{:active, true}
]
case :gen_udp.open(port, socket_opts) do
{:ok, socket} ->
{:ok,
%State{
socket: socket,
ifname: ifname,
subnet: subnet,
subnet_mask: subnet_mask,
our_ip_address: our_ip_address,
their_ip_address: their_ip_address
}}
{:error, :einval} ->
Logger.error("OneDHCPD can't open port #{port} on #{ifname}. Check permissions")
{:stop, :check_port_and_ifname}
{:error, other} ->
{:stop, other}
end
end
def handle_info({:udp, socket, _ip, @dhcp_client_port, packet}, %State{socket: socket} = state) do
case Message.decode(packet) do
{:error, _reason} ->
# Bad packet?
{:noreply, state}
message ->
message_type = Keyword.get(message.options, :dhcp_message_type)
handle_dhcp(message_type, message, state)
end
end
def handle_info(data, state) do
Logger.error("dhcpd: not sure what to do with #{inspect(data)}")
{:noreply, state}
end
defp handle_dhcp(:discover, message, state) do
Logger.debug("Responding to DHCP discover on #{state.ifname}")
# Handle a DHCP Discover message
response =
Message.response(message)
|> Map.put(:yiaddr, state.their_ip_address)
|> Map.put(:siaddr, state.our_ip_address)
|> Map.put(:options,
dhcp_message_type: :offer,
subnet_mask: state.subnet_mask,
dhcp_server_identifier: state.our_ip_address,
dhcp_lease_time: 86400
)
dhcp_packet = Message.encode(response)
:ok = :gen_udp.send(state.socket, @ip_broadcast, @dhcp_client_port, dhcp_packet)
{:noreply, state}
end
defp handle_dhcp(:request, message, state) do
Logger.debug("Responding to DHCP request on #{state.ifname}")
if message.options[:dhcp_requested_address] == state.their_ip_address do
# Send an DHCP ack
response =
Message.response(message)
|> Map.put(:yiaddr, state.their_ip_address)
|> Map.put(:siaddr, state.our_ip_address)
|> Map.put(:options,
dhcp_message_type: :ack,
subnet_mask: state.subnet_mask,
dhcp_server_identifier: state.our_ip_address,
dhcp_lease_time: message.options[:dhcp_lease_time] || 86400
)
# Update our ARP cache so that we can send a response
# directly to the client device.
:ok = ARP.replace(state.ifname, state.their_ip_address, message.chaddr)
dhcp_packet = Message.encode(response)
:ok = :gen_udp.send(state.socket, state.their_ip_address, @dhcp_client_port, dhcp_packet)
else
# Send a DHCP nak
response =
Message.response(message)
|> Map.put(:broadcast_flag, 1)
|> Map.put(:siaddr, state.our_ip_address)
|> Map.put(:options,
dhcp_message_type: :nak,
dhcp_server_identifier: state.our_ip_address,
dhcp_message: "requested address not available"
)
dhcp_packet = Message.encode(response)
:ok = :gen_udp.send(state.socket, @ip_broadcast, @dhcp_client_port, dhcp_packet)
end
{:noreply, state}
end
defp handle_dhcp(_message_type, message, state) do
Logger.info("Ignoring DHCP message: #{inspect(message)}")
{:noreply, state}
end
def terminate(_, state) do
:gen_udp.close(state.socket)
end
end