250 lines
12 KiB
Org Mode
250 lines
12 KiB
Org Mode
#+TITLE: DHCPv6-PD - First steps
|
||
#+DATE: 2024-05-26
|
||
* Prologue
|
||
The single most requested feature missing in OpenBSD base directed at
|
||
me is DHCPv6-PD. Recently I got a working setup at home using [[https://roy.marples.name/projects/dhcpcd][dhcpcd]]
|
||
from ports and a donated Fritz!Box 6660 Cable[fn::Thanks again
|
||
Mischa & Ibsen!][fn::The CPE from my ISP was just too broken to work with, let
|
||
alone develop against. It only ever hands out a single prefix and
|
||
would need a factory reset afterwards. It also does not route the
|
||
delegated prefix but depends on [[https://www.rfc-editor.org/rfc/rfc4389][ND Proxy.]]]. Time to hack on this.
|
||
* DHCPv6-PD
|
||
[[https://www.rfc-editor.org/rfc/rfc8415][DHCPv6]] is not wildly deployed outside of enterprise
|
||
networks[fn::Because of android refusing to implement it.]. DHCP for
|
||
Prefix Delegation (DHCPv6-PD) on the other hand is *the standard* to
|
||
get IPv6 prefixes into home networks. [[https://www.rfc-editor.org/rfc/rfc8415#section-6.3][RFC 8415]] has this:
|
||
#+begin_quote
|
||
It is appropriate for situations in which the delegating router (1)
|
||
does not have knowledge about the topology of the networks to which
|
||
the requesting router is attached and (2) does not require other
|
||
information aside from the identity of the requesting router to choose
|
||
a prefix for delegation. This mechanism is appropriate for use by an
|
||
ISP to delegate a prefix to a subscriber, where the delegated prefix
|
||
would possibly be subnetted and assigned to the links within the
|
||
subscriber's network.
|
||
#+end_quote
|
||
* Transmogrifying dhcpleased(8)
|
||
This not being my first rodeo, it took me about 4 hours over a weekend
|
||
to transmogrify [[https://man.openbsd.org/dhcpleased.8][=dhcpleased(8)=]] into =dhcp6leased(8)= and have it talk
|
||
to my Fritz!Box. I have also setup [[https://www.isc.org/kea/][ISC's Kea DHCP server]] for easier
|
||
development and to not risk my production network at home. Of course
|
||
it is not yet able to configure the system, but it can request a
|
||
prefix delegation from the server and parse the response. This is
|
||
enough to play with the protocol and work on the grammar for the
|
||
configuration file.
|
||
* Describing network topology
|
||
=dhcp6leased(8)= will not just request an IPv6 prefix delegation but
|
||
also use the delegated prefix to assign prefixes to downstream network
|
||
interfaces. [[https://man.openbsd.org/rad.8][rad(8)]] can then be used to send router advertisements for
|
||
clients to get IPv6 connectivity on different subnets in the home
|
||
network.
|
||
|
||
The typical use case is probably to have a few networks connected to
|
||
the OpenBSD router using vlans[fn::Maybe one vlan for WiFi, one for IOT
|
||
and one for guest WiFi.] and assign =/64= prefixes to each one of
|
||
them.
|
||
|
||
A more advanced use case would be to assign prefixes of different
|
||
lengths to the vlan interfaces. For example I have a whole (virtual)
|
||
network lab hanging off of an OpenBSD router which is not a single
|
||
flat network. I need to assign a =/60= to that interface[fn::The IETF
|
||
never managed to fully standardize this. I hear this is where homenet
|
||
failed. A less ambitious working group is working in this problem
|
||
space now: [[https://datatracker.ietf.org/wg/snac/about/][Stub Network Auto Configuration for IPv6 (snac)]]. But they
|
||
only want to deal with flat networks.] to have enough space to subnet
|
||
further.
|
||
|
||
Now, DHCPv6-PD allows us to request multiple prefixes. We could just
|
||
punt the problem of splitting a bigger prefix into smaller prefixes to
|
||
the DHCPv6 server. However, the [[https://www.rfc-editor.org/rfc/rfc8415#section-6.6][RFC has this]]:
|
||
|
||
#+begin_quote
|
||
In principle, DHCP allows a client to request new prefixes to be
|
||
delegated by sending additional IA_PD options (see Section 21.21).
|
||
However, a typical operator usually prefers to delegate a single,
|
||
larger prefix. In most deployments, it is recommended that the client
|
||
request a larger prefix in its initial transmissions rather than
|
||
request additional prefixes later on.
|
||
#+end_quote
|
||
|
||
And indeed, the Fritz!Box only gives us one prefix. We can hand the
|
||
prefix back and request a larger one, but it will only honour a single
|
||
=IA_PD= option in a solicit message.
|
||
|
||
This means we have to split up the prefix ourselves. This is perfectly
|
||
simple if we are only dealing with =/64= networks. Just count the
|
||
networks, round up to the nearest power of two and calculate the
|
||
required prefix size from that.
|
||
|
||
This gets more complicated if the prefix lengths for our sub-networks
|
||
are non-uniform, like in the more advanced use case.
|
||
|
||
I went a bit on a tangent and tried to solve this for the general
|
||
case. That means arbitrary subnet sizes and an optimal packing in the
|
||
delegated prefix. I think that would come down to the [[https://en.wikipedia.org/wiki/Bin_packing_problem][Bin packing
|
||
problem]] which is... annoying[fn::Otherwise known as NP-hard.].
|
||
|
||
I then noticed that we want a stable assignment, meaning when we add
|
||
or remove an interface we do not want to renumber all the existing and
|
||
remaining interfaces. Which would happen if try to come up with an
|
||
optimal solution because prefix assignments would most likely shift
|
||
around every time we change something.
|
||
* dhcpcd's solution
|
||
At this point I was somewhat stuck and I had a look at how dhcpcd
|
||
deals with this. While I was already using dhcpcd in my network, I had
|
||
not yet setup the more advanced use case with a =/60= and multiple
|
||
=/64=. I was pretty sure that dhcpcd can handle this, but I did not
|
||
yet know how.
|
||
|
||
Disclaimer: What follows are my notes on how I got it to work. It is
|
||
likely that I am doing things wrong and misunderstand some
|
||
parts. Unfortunately I no longer have access to GitHub[fn::An
|
||
alternative reading is: I refuse to use it because they decided I am a
|
||
suplier, which I am not. And they locked me out of my account.], so I
|
||
cannot open an issue with the project to ask for help with this. I am
|
||
very sorry.
|
||
|
||
Here is the relevant part from the [[https://man.freebsd.org/cgi/man.cgi?query=dhcpcd.conf&apropos=0&sektion=0&manpath=FreeBSD+14.0-RELEASE+and+Ports&arch=default&format=html][dhcpcd.conf man page:]]
|
||
#+begin_example
|
||
ia_pd [iaid [/ prefix / prefix_len] [interface [/ sla_id [/ prefix_len
|
||
[/ suffix]]]]]
|
||
Request a DHCPv6 Delegated Prefix for iaid. This option must
|
||
be used in an interface block. Unless a sla_id of 0 is as-
|
||
signed with the same resultant prefix length as the delegation,
|
||
a reject route is installed for the Delegated Prefix to stop
|
||
unallocated addresses being resolved upstream. If no interface
|
||
is given then we will assign a prefix to every other interface
|
||
with a sla_id equivalent to the interface index assigned by the
|
||
OS. Otherwise addresses are only assigned for each interface
|
||
and sla_id. To avoid delegating to any interface, use - as the
|
||
invalid interface name. Each assigned address will have a
|
||
suffix, defaulting to 1. If the suffix is 0 then a SLAAC ad-
|
||
dress is assigned. You cannot assign a prefix to the request-
|
||
ing interface unless the DHCPv6 server supports the RFC 6603
|
||
Prefix Exclude Option. dhcpcd has to be running for all the
|
||
interfaces it is delegating to. A default prefix_len of 64 is
|
||
assumed, unless the maximum sla_id does not fit. In this case
|
||
prefix_len is increased to the highest multiple of 8 that can
|
||
accommodate the sla_id. sla_id is an integer which must be
|
||
unique inside the iaid and is added to the prefix which must
|
||
fit inside prefix_len less the length of the delegated prefix.
|
||
You can specify multiple interface / sla_id / prefix_len per
|
||
ia_pd, space separated. IPv6RS should be disabled globally
|
||
when requesting a Prefix Delegation.
|
||
#+end_example
|
||
I kinda do not know what all of this means.
|
||
|
||
After much experimentation I ended up with this working-ish
|
||
configuration:
|
||
#+begin_example
|
||
ia_pd 2/::/59 vether0/0/60 vether1/1/64
|
||
#+end_example
|
||
which put this in =daemon.log=:
|
||
#+begin_example
|
||
vio1: delegated prefix 2001:db8:3::/56
|
||
vether0: adding address 2001:db8:3::1/60
|
||
vether1: adding address 2001:db8:3:1::1/64
|
||
#+end_example
|
||
|
||
A closer look shows that the two prefixes overlap though:
|
||
#+begin_src python
|
||
>>> import ipaddress
|
||
>>> a = ipaddress.ip_network('2001:db8:3::/60')
|
||
>>> b = ipaddress.ip_network('2001:db8:3:1::/64')
|
||
>>> a.overlaps(b)
|
||
True
|
||
#+end_src
|
||
|
||
This configuration produces non-overlapping prefix assignments:
|
||
#+begin_example
|
||
ia_pd 2/::/59 vether0/0/60 vether1/16/64
|
||
#+end_example
|
||
|
||
#+begin_example
|
||
vio1: delegated prefix 2001:db8:3::/56
|
||
vether0: adding address 2001:db8:3::1/60
|
||
vether1: adding address 2001:db8:3:10::1/64
|
||
#+end_example
|
||
|
||
Taking this apart, token by token:
|
||
+ =ia_pd= :: This is just the keyword to request a prefix delegation.
|
||
+ =2/::/59= :: 2 is a unique request ID needed by the DHCPv6
|
||
protocol. =::= is the unspecified prefix and 59 is the requested
|
||
prefix length. Since the DHCPv6 server does not have an address pool
|
||
for =/59= it hands out a prefix for the next larger prefix for which
|
||
it does have a pool, =/56= in this case.
|
||
+ =vether0/0/60= :: This assigns the 1st (index 0) =/60= prefix to
|
||
=vether0=.
|
||
+ =vether1/16/64= :: This assigns the 17th[fn::We are starting to
|
||
count at 0.] (index 16) =/64= prefix to =vether1=.
|
||
|
||
What I misunderstood when I used =vether1/1/64= was that =sla_id=
|
||
(the 1 in the middle) does not mean use the next free =/64= but use
|
||
the 2nd =/64= in the delegated prefix.
|
||
|
||
I find this confusing because the way I think about subnetting is that
|
||
the different prefixes do not stand alone. =2001:db8:3:10::/64= is not
|
||
the 17th =/64= prefix in =2001:db8:3::/56= but the first =/64= in the
|
||
2nd =/60=. It's a hierarchy.
|
||
|
||
* Next steps
|
||
dhcpcd puts a lot of work on the administrator to get the subnet
|
||
assignments just right. It neatly avoids[fn::You could say it punts
|
||
them to the administrator.] the problems I had identified. The
|
||
assignments are stable and the algorithm is not massively expensive.
|
||
|
||
This got me unstuck and I have an idea how =dhcp6leased(8)= should be
|
||
configured.
|
||
1. It should work out automatically the size of the prefix it
|
||
requests. I was under the impression that dhcpcd would also do
|
||
that, but it did not work. Probably my mistake somewhere.
|
||
2. Assignments are listed in order and =dhcpleased(8)= will work out
|
||
the boundaries.
|
||
|
||
I am not sure about the exact syntax, but as an example, consider
|
||
this:
|
||
|
||
#+begin_example
|
||
request prefix delegation on vio0 for {
|
||
vether0/60
|
||
reserve/60
|
||
vether1/64
|
||
vether2/64
|
||
vether3/60
|
||
}
|
||
#+end_example
|
||
|
||
It would request a =/58= which fits 4 =/60=. We assign the first =/60=
|
||
to =vether0=, keep the next =/60= in reserve in case we want to add
|
||
interfaces between =vether0= and =vether1= in the future without
|
||
triggering a renumber. We then pick the first =/64= out of the third
|
||
=/60= and assign it to =vether1=. We still have space in the third
|
||
=/60= to assign a =/64= to =vether2=. We pick the fourth and last
|
||
=/60= and assign it to =vether3=:
|
||
|
||
#+begin_example
|
||
vether0 2001:db8:3::/60
|
||
reserve 2001:db8:3:10::/60
|
||
vether1 2001:db8:3:20::/64
|
||
vether2 2001:db8:3:21::/64
|
||
vether3 2001:db8:3:30::/60
|
||
#+end_example
|
||
|
||
I think I have code that can do this and it is not overly
|
||
complicated. It can currently only handle the upper 64 bits of an IPv6
|
||
address because it does math on =uint64_t=. I will try to extend it to
|
||
the lower half so that we can assign something like =/96= to a link,
|
||
even if that means that half the [[https://datatracker.ietf.org/wg/6man/about/][IPv6 Maintenance (6man)]] IETF working
|
||
group will hunt me down.
|
||
|
||
* Epilogue
|
||
This strikes a slightly better balance between work that needs to be
|
||
done by the administrator and help the tool provides compared to what
|
||
dhcpcd implements. But I would not have come up with this without
|
||
prior work by dhcpcd, kudos to Roy.
|
||
|
||
Coming up with an addressing plan is still hard work, so I will
|
||
implement a feature in =dhcp6leased= to have it output the addressing
|
||
plan it worked out as a configuration check before going to
|
||
work. Because renumbering is hard.
|