ex_dhcp v0.1.5 ExDhcp behaviour View Source
Creates an OTP-compliant DHCP packet server.
An ExDhcp module binds to a UDP port, listens to DHCP messages sent to the port, and can resend DHCP messages (typically as a broadcast message) in response.
You may instrument whatever state you would like into the server; the DHCP-specific contents will be encapsulated into the internals of the server itself and all of the state exposed to the callbacks will be your own state.
Please consult the readme for crucial deployment information, as you won't be able immediately see or serve DHCP packets without some configuration external to the Erlang/Elixir VM.
ExDhcp is not a fully-functional DHCP server which conforms to the RFC 1531 specs.
It does not:
- accept configuration for handing out DHCP leases
- keep the state of the DHCP leases
- store DHCP lease information to durable storage
- manage arp tables
However, you could use ExDhcp to implement that functionality. For example, minimal ExDhcp server might look something like this:
defmodule MyDhcpServer do
use ExDhcp
alias ExDhcp.Packet
def start_link(init_state) do
ExDhcp.start_link(__MODULE__, init_state)
end
@impl true
def init(init_state), do: {:ok, init_state}
@impl true
def handle_discover(request, xid, mac, state) do
# insert code here. Should assign the unimplemented values
# for the response below:
response = Packet.respond(request, :offer,
yiaddr: issued_your_address,
siaddr: server_ip_address,
subnet_mask: subnet_mask,
routers: [router],
lease_time: lease_time,
server: server_ip_address,
domain_name_servers: [dns_server]))
{:respond, response, new_state}
end
@impl true
def handle_request(request, xid, mac, state) do
# insert code here.
response = Packet.respond(request, :ack,
yiaddr: issued_your_address ...)
{:respond, response, state}
end
@impl true
def handle_decline(request, xid, mac, state) do
# insert code here.
response = Packet.respond(request, :offer,
yiaddr: new_issued_address ...)
{:respond, response, state}
end
end
if you intend to ignore the particular DHCP packet, instead emit the following return from the callback:
{:norespond, state}
You must implement the following three DHCP functionalities as callbacks to successfully assign network hosts:
handle_discover/4
handle_request/4
handle_decline/4
You may also want to implement these callbacks:
handle_inform/4
to manage a DHCP client requesting early infohandle_release/4
to manage DHCP clients relinquishing their leaseshandle_packet/4
to generically handle DHCP packets- this is most useful to monitor how other DHCP servers are responding on your network
Each of these DHCP callbacks take four arguments, in order:
- the fully-parsed
ExDhcp.Packet
structure, - the xid (transaction id)
- the mac address of the client
- the current state of the ExDhcp GenServer
These are provided as arguments to enable you to easily trap their contents in pattern-matching guards.
NB: the ExDhcp.Packet
structure has an :options
field, and you
may want to emit tags that are outside of the provided basic dhcp
options
(see ExDhcp.Options.Basic
). In this case, you should implement your
own parser module (see ExDhcp.Options.Macro
) and instrument it into your
ExDhcp module using the :dhcp_options
option like so:
use Dhcp, dhcp_options: [ExDhcp.Options.Basic, YourParserModule, AnotherParserModule]
ExDhcp also provides these optional callbacks
handle_call/3
handle_cast/2
handle_continue/2
handle_info/2
These operate identically to their counterparts in GenServer
, but note that they are passed
their encapsulated state, not the raw state of the GenServer.
Supervising an ExDhcp instance
The recommended supervision strategy is a single ExDhcp server under the main
application. This can be achieved using the ExDhcp.Supervisor
helper module.
defmodule MyProject.Application
def start(_type, _args) do
initial_value = ...
dhcp_opts = ...
children = [{ExDhcp.Supervisor, {MyModule, initial_value, dhcp_opts}}]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
For Testing
ExDhcp will instrument inside of your module the b:port/1
call which will
return the UDP port that your DHCP server is listening to, this is useful
when you are running async tests and you assign your initial port to 0.
Note if you are testing supervised, and your dhcp process dies, then the port may be rebound to another number if you set it 0 initially.
Link to this section Summary
Types
DHCP callbacks should provide either a :respond
or :norespond
outcome.
Functions
Returns a specification to start this module under a supervisor.
returns the port that the dhcp server is listening to
Caller function initiating the spawning of your ExDhcp GenServer.
Callbacks
Responds to the DHCP decline query, as encoded in option 53.
Responds to the DHCP discover query, as encoded in option 53.
Responds to the DHCP inform query, as encoded in option 53. Defaults to ignore.
Responds to other DHCP queries or broadcast packet types your typical server may not be setup to listen to.
Responds to the DHCP release query, as encoded in option 53. Defaults to ignore.
Responds to the DHCP request query, as encoded in option 53.
Invoked on the new DHCP server process when started by ExDhcp.start_link/3
returns the UDP port that the DHCP server listens to
Link to this section Types
response()
View Source
response() ::
{:respond, ExDhcp.Packet.t(), new_state :: term()}
| {:norespond, new_state :: any()}
| {:stop, reason :: term(), new_state :: term()}
response() :: {:respond, ExDhcp.Packet.t(), new_state :: term()} | {:norespond, new_state :: any()} | {:stop, reason :: term(), new_state :: term()}
DHCP callbacks should provide either a :respond
or :norespond
outcome.
In the case of the
:respond
outcome, a UDP response is sent over the open port earmarked for the specified mac address.The
:norespond
outcome is a no-op. No information is sent over the wire, and the client may choose to either continue sending requests on the presumption that the UDP packets were dropped, or initiate an entirely new transaction.
Link to this section Functions
child_spec(init_arg) View Source
Returns a specification to start this module under a supervisor.
See Supervisor
.
port(srv)
View Source
port(GenServer.server()) :: {:ok, :inet.port_number()} | {:error, any()}
port(GenServer.server()) :: {:ok, :inet.port_number()} | {:error, any()}
returns the port that the dhcp server is listening to
start_link(module, initializer, options \\ []) View Source
Caller function initiating the spawning of your ExDhcp GenServer.
You may supply the following extra options:
:port
the UDP port you'd like ExDhcp to listen in on; defaults to6767
. you may want to change this to67
if you do not want to use iptables to redirect DHCP transactions to a nonprivileged Elixir server.:ip
the IP that you'd like the ExDhcp port to be bound to. (nb: This feature is currently untested):bind_to_device
(must be a binary string), the device you would like to bind to for DHCP listening.This is most useful when you have a device with a internal-and -external-facing net interfaces and you would like to only respond to DHCP requests coming from select directions.
This requires
cap_net_raw
to be set. In Linux systems, this is settable as superuser using the following command:setcap cap_net_raw=ep /path/to/beam.smp
:client_port
specify a nonstandard port to send the response to. Most useful for testing purposes; defaults to68
.:broadcast_addr
to specify a nonstandard address for responses. Defaults to{255, 255, 255, 255}
.
See GenServer.start_link/3
for further options.
Link to this section Callbacks
handle_call(term, from, state)
View Source
(optional)
handle_call(term(), from :: GenServer.from(), state :: term()) ::
{:reply, reply :: term(), new_state :: term()}
| {:reply, reply :: term(), new_state :: term(),
timeout() | :hibernate | {:continue, term()}}
| {:noreply, new_state :: term()}
| {:noreply, new_state :: term(),
timeout() | :hibernate | {:continue, term()}}
| {:stop, reason :: term(), reply :: term(), new_state :: term()}
| {:stop, reason :: term(), new_state :: term()}
handle_call(term(), from :: GenServer.from(), state :: term()) :: {:reply, reply :: term(), new_state :: term()} | {:reply, reply :: term(), new_state :: term(), timeout() | :hibernate | {:continue, term()}} | {:noreply, new_state :: term()} | {:noreply, new_state :: term(), timeout() | :hibernate | {:continue, term()}} | {:stop, reason :: term(), reply :: term(), new_state :: term()} | {:stop, reason :: term(), new_state :: term()}
handle_cast(request, state) View Source (optional)
handle_continue(continue, state) View Source (optional)
handle_decline(packet, xid, mac_addr, state)
View Source
handle_decline(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_decline( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to the DHCP decline query, as encoded in option 53.
handle_discover(packet, xid, mac_addr, state)
View Source
handle_discover(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_discover( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to the DHCP discover query, as encoded in option 53.
handle_info(term, state) View Source (optional)
handle_inform(packet, xid, mac_addr, state)
View Source
(optional)
handle_inform(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_inform( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to the DHCP inform query, as encoded in option 53. Defaults to ignore.
handle_packet(packet, xid, mac_addr, state)
View Source
(optional)
handle_packet(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_packet( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to other DHCP queries or broadcast packet types your typical server may not be setup to listen to.
Leader contention or race conditions may arise in certain situations. For example, a DHCP request might have been handled by another server already and broadcasted over the layer 2 network. To avoid these issues, your server may want to act on its internal state based on the information transmitted in these packets.
Use this callback to implement these features.
Server queries you might also want to monitor are:
- DHCP_OFFER (2)
- DHCP_ACK (5)
- DHCP_NAK (6)
You should also use a custom handle_packet
routine if you override ExDhcp.Options.Basic
with your own DHCP options parser that overwrites option 53 with a different atom/value
assignment scheme.
handle_release(packet, xid, mac_addr, state)
View Source
(optional)
handle_release(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_release( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to the DHCP release query, as encoded in option 53. Defaults to ignore.
handle_request(packet, xid, mac_addr, state)
View Source
handle_request(
packet :: ExDhcp.Packet.t(),
xid :: non_neg_integer(),
mac_addr :: ExDhcp.Utils.mac(),
state :: term()
) :: response()
handle_request( packet :: ExDhcp.Packet.t(), xid :: non_neg_integer(), mac_addr :: ExDhcp.Utils.mac(), state :: term() ) :: response()
Responds to the DHCP request query, as encoded in option 53.
init(term) View Source (optional)
Invoked on the new DHCP server process when started by ExDhcp.start_link/3
Will typically emit {:ok, state}
, where state
is the initial state
you expect to be contained within your ExDhcp GenServer.
use init/1
if you don't care about accessing the underlying socket;
use init/2
if you do. ExDhcp will default to using init/2
and fallback
to init/1
.
init(term, arg2) View Source (optional)
port(arg1)
View Source
port(GenServer.server()) :: {:ok, :inet.port_number()} | {:error, any()}
port(GenServer.server()) :: {:ok, :inet.port_number()} | {:error, any()}
returns the UDP port that the DHCP server listens to