DHCPv6-PD - First steps
This commit is contained in:
parent
5275adb20c
commit
f5886f370f
249
dhcpv6-pd-first-steps.org
Normal file
249
dhcpv6-pd-first-steps.org
Normal file
@ -0,0 +1,249 @@
|
||||
#+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!][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.
|
Loading…
Reference in New Issue
Block a user