OpenBSD Journal

dhcpd(8): use UDP sockets instead of BPF

Contributed by Peter N. M. Hansteen on from the modernizing BPFoonery dept.

In some cases, the current dhcpd(8) is not quite as reliable as one would want in providing the requested data to the actual requestor. After some rounds of discussion and experimentation, David Gwynne (dlg@) is circulating a diff on tech@ that switches the daemon to use UDP sockets instead of bpf.

The motivation is summarized as,

tl;dr this replaces bpf with udp sockets in dhcpd, mostly to make it
better at replying with the ip that requests were sent to.

and the full message, with the subject dhcpd(8): use UDP sockets instead of BPF reads,

List:       openbsd-tech
Subject:    dhcpd(8): use UDP sockets instead of BPF
From:       David Gwynne <david () gwynne ! id ! au>
Date:       2025-06-13 3:29:20

tl;dr this replaces bpf with udp sockets in dhcpd, mostly to make it
better at replying with the ip that requests were sent to.

ive been hacking on this because of a problem at work, which i want to
solve by setting up a bunch of "anycast" dhcp servers. ie, i want to
have multiple dhcpd on separate servers with the same IP assigned
as an alias on all of them.
bpf does not make this easy. the bpf code unconditionally steals
all dhcp packets entering the interface, regardless of dst ip, and
unconditionally replies to them using the primary address on the
interface. the "anycast" ip is effectively lost. if i use dhcpd -u,
i am limited to handling DHCPINFORM messages.

florian and i have been talking on and off for a few years about whether
it's possible to implement a dhcp client using udp sockets rather than
bpf, so i took the learnings from those experiments and tried to hack
them into dhcpd.

this removes bpf from dhcpd.

instead, each interface has a udp listener for it's primary IP. eg, i
have etherip0 with 192.168.0.1 on it, so i bind a udp socket to
192.168.0.1.

if i have at least one of these interface listeners, then i also
bind a socket to 255.255.255.255. this allows me to receive packets
from dhcp clients attempting their initial DISCOVER. packets received
on this socket have the IP_RECVIF sockopt enabled, which lets me
marry the request up with which interface it was recved on.

replies to these DISCOVER packets will come from the interface IP, ie,
192.168.0.1 in my setup. to support clients that want the replies
broadcast to 255.255.255.255, the IP_MULTICAST_IF and SO_BROADCAST
sockopts are enabled on the interface socket. IP_MULTICAST_IF is
necessary to tell the kernel which interface a packet to 255.255.255.255
is supposed to come out.

however, if the broadcast flag is not set, then the server is supposed
to unicast the reply to the client. the problem with this is udp sockets
will rely on ARP to know where to send that reply to, but the client
hasn't got an address yet, so it wont reply to that arp request.

the solution to this problem is i inject a route into the kernel with
the ip to ethernet address mapping in it. because only a process that
currently has root privs can add routes, ive made a stupid little helper
process that proxies the route addtion.

further requests to and replies from the dhcp server should go to and
come from this interface ip respectively.

however, unless i assign my "anycast" ip as the primary address on a
supported interface, this doesnt help me. i'd have to create a vether
interface with the anycast IP, but unless i enable ip forwarding i wont
be able to receive these packets coming from the real interface im
connected with.

so i've tweaked the udpsock code to be more usable. part of it is
changing the udpsock handling inside the guts of dhcpd so it is allowed
to handle relayed requests (ie, giaddr in the request is != 0.0.0.0).
the other tweaks were to udpsock itself, basically letting it use
IP_SENDSRC cmsgs so if you have a wildcard listener it will do it's best
to reply from the expected address.

to support all the above i had to carry the addresses a messages was
recv()ed with all the way through to where the relevant send()s are.
this was more annoying than i thought cos some replies are deferred
(thanks icmp).

while i've tried to make dhcpd work the same as it did before this
change, there is a big semantic difference that's outside it's control.
bpf operated before pf, so you didn't have to write rules in pf.conf to
allow dhcpd to work. because udp socket processing happens as part of
the network stack, dhcp packets are now subject to pf. if you have a
default deny ruleset, you have to explicitly allow dhcp packets in your
ruleset. i'm using something like this at home:

pass in on vport107 inet proto udp to { vport107:0 255.255.255.255 } port bootps
pass out quick on vport107 inet proto udp from vport107:0 port bootps to port bootpc

i've been using this in production as the backend for dhcp relays to
talk to for a couple of weeks now, and at home with a bunch stupid
devices talking directly to the udp sockets on the local net.

Followed by the code that implements the change, in a diff that will require a recent -current checkout.

If any of this sounds familiar to you or you are simply feeling adventurous, this is a chance to test and report back any observations.


Comments
  1. By n/a (Cabal) on

    Hopefully dhcpleased in the future!

Credits

Copyright © - Daniel Hartmeier. All rights reserved. Articles and comments are copyright their respective authors, submission implies license to publish on this web site. Contents of the archive prior to as well as images and HTML templates were copied from the fabulous original deadly.org with Jose's and Jim's kind permission. This journal runs as CGI with httpd(8) on OpenBSD, the source code is BSD licensed. undeadly \Un*dead"ly\, a. Not subject to death; immortal. [Obs.]