diff --git a/dhcpv6-pd-first-steps.org b/dhcpv6-pd-first-steps.org new file mode 100644 index 0000000..23f3f3e --- /dev/null +++ b/dhcpv6-pd-first-steps.org @@ -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.