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 info
  • handle_release/4 to manage DHCP clients relinquishing their leases
  • handle_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:

  1. the fully-parsed ExDhcp.Packet structure,
  2. the xid (transaction id)
  3. the mac address of the client
  4. 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

Link to this type

response() View Source
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

Returns a specification to start this module under a supervisor.

See Supervisor.

returns the port that the dhcp server is listening to

Link to this function

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 to 6767. you may want to change this to 67 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 to 68.

  • :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

Link to this callback

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()}

See GenServer.handle_call/3

Link to this callback

handle_cast(request, state) View Source (optional)
handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}

See GenServer.handle_cast/2

Link to this callback

handle_continue(continue, state) View Source (optional)
handle_continue(continue :: term(), state :: term()) ::
  {:noreply, new_state :: term()}
  | {:noreply, new_state :: term(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: term()}

See GenServer.handle_continue/2

Link to this callback

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()

Responds to the DHCP decline query, as encoded in option 53.

Link to this callback

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()

Responds to the DHCP discover query, as encoded in option 53.

Link to this callback

handle_info(term, state) View Source (optional)
handle_info(term(), state()) ::
  {:noreply, new_state :: state()}
  | {:noreply, new_state :: state(),
     timeout() | :hibernate | {:continue, term()}}
  | {:stop, reason :: term(), new_state :: state()}

See GenServer.handle_info/2

Link to this callback

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()

Responds to the DHCP inform query, as encoded in option 53. Defaults to ignore.

Link to this callback

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()

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.

Link to this callback

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()

Responds to the DHCP release query, as encoded in option 53. Defaults to ignore.

Link to this callback

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()

Responds to the DHCP request query, as encoded in option 53.

Link to this callback

init(term) View Source (optional)
init(term()) ::
  {:ok, term()}
  | {:ok, term(), timeout() | :hibernate | {:continue, term()}}
  | :ignore
  | {:stop, reason :: any()}

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.

Link to this callback

init(term, arg2) View Source (optional)
init(term(), :gen_udp.socket()) ::
  {:ok, term()}
  | {:ok, term(), timeout() | :hibernate | {:continue, term()}}
  | :ignore
  | {:stop, reason :: any()}

returns the UDP port that the DHCP server listens to

Link to this callback

terminate(condition, state) View Source (optional)
terminate(
  condition :: :normal | :shutdown | {:shutdown, term()},
  state :: term()
) :: term()

See GenServer.terminate/2