plugin.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. // Copyright 2018-present the CoreDHCP Authors. All rights reserved
  2. // This source code is licensed under the MIT license found in the
  3. // LICENSE file in the root directory of this source tree.
  4. // Package prefix implements a plugin offering prefixes to clients requesting them
  5. // This plugin attributes prefixes to clients requesting them with IA_PREFIX requests.
  6. //
  7. // Arguments for the plugin configuration are as follows, in this order:
  8. // - prefix: The base prefix from which assigned prefixes are carved
  9. // - max: maximum size of the prefix delegated to clients. When a client requests a larger prefix
  10. // than this, this is the size of the offered prefix
  11. package prefix
  12. // FIXME: various settings will be hardcoded (default size, minimum size, lease times) pending a
  13. // better configuration system
  14. import (
  15. "bytes"
  16. "errors"
  17. "fmt"
  18. "net"
  19. "strconv"
  20. "sync"
  21. "time"
  22. "github.com/bits-and-blooms/bitset"
  23. "github.com/insomniacslk/dhcp/dhcpv6"
  24. dhcpIana "github.com/insomniacslk/dhcp/iana"
  25. "github.com/coredhcp/coredhcp/handler"
  26. "github.com/coredhcp/coredhcp/logger"
  27. "github.com/coredhcp/coredhcp/plugins"
  28. "github.com/coredhcp/coredhcp/plugins/allocators"
  29. "github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
  30. )
  31. var log = logger.GetLogger("plugins/prefix")
  32. // Plugin registers the prefix. Prefix delegation only exists for DHCPv6
  33. var Plugin = plugins.Plugin{
  34. Name: "prefix",
  35. Setup6: setupPrefix,
  36. }
  37. const leaseDuration = 3600 * time.Second
  38. func setupPrefix(args ...string) (handler.Handler6, error) {
  39. // - prefix: 2001:db8::/48 64
  40. if len(args) < 2 {
  41. return nil, errors.New("Need both a subnet and an allocation max size")
  42. }
  43. _, prefix, err := net.ParseCIDR(args[0])
  44. if err != nil {
  45. return nil, fmt.Errorf("Invalid pool subnet: %v", err)
  46. }
  47. allocSize, err := strconv.Atoi(args[1])
  48. if err != nil || allocSize > 128 || allocSize < 0 {
  49. return nil, fmt.Errorf("Invalid prefix length: %v", err)
  50. }
  51. // TODO: select allocators based on heuristics or user configuration
  52. alloc, err := bitmap.NewBitmapAllocator(*prefix, allocSize)
  53. if err != nil {
  54. return nil, fmt.Errorf("Could not initialize prefix allocator: %v", err)
  55. }
  56. return (&Handler{
  57. Records: make(map[string][]lease),
  58. allocator: alloc,
  59. }).Handle, nil
  60. }
  61. type lease struct {
  62. Prefix net.IPNet
  63. Expire time.Time
  64. }
  65. // Handler holds state of allocations for the plugin
  66. type Handler struct {
  67. // Mutex here is the simplest implementation fit for purpose.
  68. // We can revisit for perf when we move lease management to separate plugins
  69. sync.Mutex
  70. // Records has a string'd []byte as key, because []byte can't be a key itself
  71. // Since it's not valid utf-8 we can't use any other string function though
  72. Records map[string][]lease
  73. allocator allocators.Allocator
  74. }
  75. // samePrefix returns true if both prefixes are defined and equal
  76. // The empty prefix is equal to nothing, not even itself
  77. func samePrefix(a, b *net.IPNet) bool {
  78. if a == nil || b == nil {
  79. return false
  80. }
  81. return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
  82. }
  83. // recordKey computes the key for the Records array from the client ID
  84. func recordKey(d dhcpv6.DUID) string {
  85. return string(d.ToBytes())
  86. }
  87. // Handle processes DHCPv6 packets for the prefix plugin for a given allocator/leaseset
  88. func (h *Handler) Handle(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
  89. msg, err := req.GetInnerMessage()
  90. if err != nil {
  91. log.Error(err)
  92. return nil, true
  93. }
  94. client := msg.Options.ClientID()
  95. if client == nil {
  96. log.Error("Invalid packet received, no clientID")
  97. return nil, true
  98. }
  99. // Each request IA_PD requires an IA_PD response
  100. for _, iapd := range msg.Options.IAPD() {
  101. if err != nil {
  102. log.Errorf("Malformed IAPD received: %v", err)
  103. resp.AddOption(&dhcpv6.OptStatusCode{StatusCode: dhcpIana.StatusMalformedQuery})
  104. return resp, true
  105. }
  106. iapdResp := &dhcpv6.OptIAPD{
  107. IaId: iapd.IaId,
  108. }
  109. // First figure out what prefixes the client wants
  110. hints := iapd.Options.Prefixes()
  111. if len(hints) == 0 {
  112. // If there are no IAPrefix hints, this is still a valid IA_PD request (just
  113. // unspecified) and we must attempt to allocate a prefix; so we include an empty hint
  114. // which is equivalent to no hint
  115. hints = []*dhcpv6.OptIAPrefix{{Prefix: &net.IPNet{}}}
  116. }
  117. // Bitmap to track which requests are already satisfied or not
  118. satisfied := bitset.New(uint(len(hints)))
  119. // A possible simple optimization here would be to be able to lock single map values
  120. // individually instead of the whole map, since we lock for some amount of time
  121. h.Lock()
  122. knownLeases := h.Records[recordKey(client)]
  123. // Bitmap to track which leases are already given in this exchange
  124. givenOut := bitset.New(uint(len(knownLeases)))
  125. // This is, for now, a set of heuristics, to reconcile the requests (prefix hints asked
  126. // by the clients) with what's on offer (existing leases for this client, plus new blocks)
  127. // Try to find leases that exactly match a hint, and extend them to satisfy the request
  128. // This is the safest heuristic, if the lease matches exactly we know we aren't missing
  129. // assigning it to a better candidate request
  130. for hintIdx, h := range hints {
  131. for leaseIdx := range knownLeases {
  132. if samePrefix(h.Prefix, &knownLeases[leaseIdx].Prefix) {
  133. expire := time.Now().Add(leaseDuration)
  134. if knownLeases[leaseIdx].Expire.Before(expire) {
  135. knownLeases[leaseIdx].Expire = expire
  136. }
  137. satisfied.Set(uint(hintIdx))
  138. givenOut.Set(uint(leaseIdx))
  139. addPrefix(iapdResp, knownLeases[leaseIdx])
  140. }
  141. }
  142. }
  143. // Then handle the empty hints, by giving out any remaining lease we
  144. // have already assigned to this client
  145. for hintIdx, h := range hints {
  146. if satisfied.Test(uint(hintIdx)) ||
  147. (h.Prefix != nil && !h.Prefix.IP.Equal(net.IPv6zero)) {
  148. continue
  149. }
  150. for leaseIdx, l := range knownLeases {
  151. if givenOut.Test(uint(leaseIdx)) {
  152. continue
  153. }
  154. // If a length was requested, only give out prefixes of that length
  155. // This is a bad heuristic depending on the allocator behavior, to be improved
  156. if hintPrefixLen, _ := h.Prefix.Mask.Size(); hintPrefixLen != 0 {
  157. leasePrefixLen, _ := l.Prefix.Mask.Size()
  158. if hintPrefixLen != leasePrefixLen {
  159. continue
  160. }
  161. }
  162. expire := time.Now().Add(leaseDuration)
  163. if knownLeases[leaseIdx].Expire.Before(expire) {
  164. knownLeases[leaseIdx].Expire = expire
  165. }
  166. satisfied.Set(uint(hintIdx))
  167. givenOut.Set(uint(leaseIdx))
  168. addPrefix(iapdResp, knownLeases[leaseIdx])
  169. }
  170. }
  171. // Now remains requests with a hint that we can't trivially satisfy, and possibly expired
  172. // leases that haven't been explicitly requested again.
  173. // A possible improvement here would be to try to widen existing leases, to satisfy wider
  174. // requests that contain an existing leases; and to try to break down existing leases into
  175. // smaller allocations, to satisfy requests for a subnet of an existing lease
  176. // We probably don't need such complex behavior (the vast majority of requests will come
  177. // with an empty, or length-only hint)
  178. // Assign a new lease to satisfy the request
  179. var newLeases []lease
  180. for i, prefix := range hints {
  181. if satisfied.Test(uint(i)) {
  182. continue
  183. }
  184. if prefix.Prefix == nil {
  185. // XXX: replace usage of dhcp.OptIAPrefix with a better struct in this inner
  186. // function to avoid repeated nullpointer checks
  187. prefix.Prefix = &net.IPNet{}
  188. }
  189. allocated, err := h.allocator.Allocate(*prefix.Prefix)
  190. if err != nil {
  191. log.Debugf("Nothing allocated for hinted prefix %s", prefix)
  192. continue
  193. }
  194. l := lease{
  195. Expire: time.Now().Add(leaseDuration),
  196. Prefix: allocated,
  197. }
  198. addPrefix(iapdResp, l)
  199. newLeases = append(knownLeases, l)
  200. log.Debugf("Allocated %s to %s (IAID: %x)", &allocated, client, iapd.IaId)
  201. }
  202. if newLeases != nil {
  203. h.Records[recordKey(client)] = newLeases
  204. }
  205. h.Unlock()
  206. if len(iapdResp.Options.Options) == 0 {
  207. log.Debugf("No valid prefix to return for IAID %x", iapd.IaId)
  208. iapdResp.Options.Add(&dhcpv6.OptStatusCode{
  209. StatusCode: dhcpIana.StatusNoPrefixAvail,
  210. })
  211. }
  212. resp.AddOption(iapdResp)
  213. }
  214. return resp, false
  215. }
  216. func addPrefix(resp *dhcpv6.OptIAPD, l lease) {
  217. lifetime := time.Until(l.Expire)
  218. resp.Options.Add(&dhcpv6.OptIAPrefix{
  219. PreferredLifetime: lifetime,
  220. ValidLifetime: lifetime,
  221. Prefix: dup(&l.Prefix),
  222. })
  223. }
  224. func dup(src *net.IPNet) (dst *net.IPNet) {
  225. dst = &net.IPNet{
  226. IP: make(net.IP, net.IPv6len),
  227. Mask: make(net.IPMask, net.IPv6len),
  228. }
  229. copy(dst.IP, src.IP)
  230. copy(dst.Mask, src.Mask)
  231. return dst
  232. }