plugin.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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/insomniacslk/dhcp/dhcpv6"
  23. dhcpIana "github.com/insomniacslk/dhcp/iana"
  24. "github.com/willf/bitset"
  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. func samePrefix(a, b *net.IPNet) bool {
  76. return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
  77. }
  78. // recordKey computes the key for the Records array from the client ID
  79. func recordKey(d *dhcpv6.Duid) string {
  80. return string(d.ToBytes())
  81. }
  82. // Handle processes DHCPv6 packets for the prefix plugin for a given allocator/leaseset
  83. func (h *Handler) Handle(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
  84. msg, err := req.GetInnerMessage()
  85. if err != nil {
  86. log.Error(err)
  87. return nil, true
  88. }
  89. client := msg.Options.ClientID()
  90. if client == nil {
  91. log.Error("Invalid packet received, no clientID")
  92. return nil, true
  93. }
  94. // Each request IA_PD requires an IA_PD response
  95. for _, iapd := range msg.Options.IAPD() {
  96. if err != nil {
  97. log.Errorf("Malformed IAPD received: %v", err)
  98. resp.AddOption(&dhcpv6.OptStatusCode{StatusCode: dhcpIana.StatusMalformedQuery})
  99. return resp, true
  100. }
  101. iapdResp := &dhcpv6.OptIAPD{
  102. IaId: iapd.IaId,
  103. }
  104. // First figure out what prefixes the client wants
  105. hints := iapd.Options.Prefixes()
  106. if len(hints) == 0 {
  107. // If there are no IAPrefix hints, this is still a valid IA_PD request (just
  108. // unspecified) and we must attempt to allocate a prefix; so we include an empty hint
  109. // which is equivalent to no hint
  110. hints = []*dhcpv6.OptIAPrefix{{}}
  111. }
  112. // Bitmap to track which requests are already satisfied or not
  113. satisfied := bitset.New(uint(len(hints)))
  114. // A possible simple optimization here would be to be able to lock single map values
  115. // individually instead of the whole map, since we lock for some amount of time
  116. h.Lock()
  117. knownLeases := h.Records[recordKey(client)]
  118. // Bitmap to track which leases are already given in this exchange
  119. givenOut := bitset.New(uint(len(knownLeases)))
  120. // This is, for now, a set of heuristics, to reconcile the requests (prefix hints asked
  121. // by the clients) with what's on offer (existing leases for this client, plus new blocks)
  122. // Try to find leases that exactly match a hint, and extend them to satisfy the request
  123. // This is the safest heuristic, if the lease matches exactly we know we aren't missing
  124. // assigning it to a better candidate request
  125. for hintIdx, h := range hints {
  126. for leaseIdx := range knownLeases {
  127. if samePrefix(h.Prefix, &knownLeases[leaseIdx].Prefix) {
  128. expire := time.Now().Add(leaseDuration)
  129. if knownLeases[leaseIdx].Expire.Before(expire) {
  130. knownLeases[leaseIdx].Expire = expire
  131. }
  132. satisfied.Set(uint(hintIdx))
  133. givenOut.Set(uint(leaseIdx))
  134. addPrefix(iapdResp, knownLeases[leaseIdx])
  135. }
  136. }
  137. }
  138. // Then handle the empty hints, by giving out any remaining lease we
  139. // have already assigned to this client
  140. for hintIdx, h := range hints {
  141. if satisfied.Test(uint(hintIdx)) ||
  142. (h.Prefix != nil && !h.Prefix.IP.Equal(net.IPv6zero)) {
  143. continue
  144. }
  145. for leaseIdx, l := range knownLeases {
  146. if givenOut.Test(uint(leaseIdx)) {
  147. continue
  148. }
  149. // If a length was requested, only give out prefixes of that length
  150. // This is a bad heuristic depending on the allocator behavior, to be improved
  151. if hintPrefixLen, _ := h.Prefix.Mask.Size(); hintPrefixLen != 0 {
  152. leasePrefixLen, _ := l.Prefix.Mask.Size()
  153. if hintPrefixLen != leasePrefixLen {
  154. continue
  155. }
  156. }
  157. expire := time.Now().Add(leaseDuration)
  158. if knownLeases[leaseIdx].Expire.Before(expire) {
  159. knownLeases[leaseIdx].Expire = expire
  160. }
  161. satisfied.Set(uint(hintIdx))
  162. givenOut.Set(uint(leaseIdx))
  163. addPrefix(iapdResp, knownLeases[leaseIdx])
  164. }
  165. }
  166. // Now remains requests with a hint that we can't trivially satisfy, and possibly expired
  167. // leases that haven't been explicitly requested again.
  168. // A possible improvement here would be to try to widen existing leases, to satisfy wider
  169. // requests that contain an existing leases; and to try to break down existing leases into
  170. // smaller allocations, to satisfy requests for a subnet of an existing lease
  171. // We probably don't need such complex behavior (the vast majority of requests will come
  172. // with an empty, or length-only hint)
  173. // Assign a new lease to satisfy the request
  174. var newLeases []lease
  175. for i, prefix := range hints {
  176. if satisfied.Test(uint(i)) {
  177. continue
  178. }
  179. if prefix.Prefix == nil {
  180. // XXX: replace usage of dhcp.OptIAPrefix with a better struct in this inner
  181. // function to avoid repeated nullpointer checks
  182. prefix.Prefix = &net.IPNet{}
  183. }
  184. allocated, err := h.allocator.Allocate(*prefix.Prefix)
  185. if err != nil {
  186. log.Debugf("Nothing allocated for hinted prefix %s", prefix)
  187. continue
  188. }
  189. l := lease{
  190. Expire: time.Now().Add(leaseDuration),
  191. Prefix: allocated,
  192. }
  193. addPrefix(iapdResp, l)
  194. newLeases = append(knownLeases, l)
  195. }
  196. if newLeases != nil {
  197. h.Records[recordKey(client)] = newLeases
  198. }
  199. h.Unlock()
  200. if len(iapdResp.Options.Options) == 0 {
  201. log.Debugf("No valid prefix to return for IAID %x", iapd.IaId)
  202. iapdResp.Options.Add(&dhcpv6.OptStatusCode{
  203. StatusCode: dhcpIana.StatusNoPrefixAvail,
  204. })
  205. }
  206. resp.AddOption(iapdResp)
  207. }
  208. return resp, false
  209. }
  210. func addPrefix(resp *dhcpv6.OptIAPD, l lease) {
  211. lifetime := time.Until(l.Expire)
  212. resp.Options.Add(&dhcpv6.OptIAPrefix{
  213. PreferredLifetime: lifetime,
  214. ValidLifetime: lifetime,
  215. Prefix: dup(&l.Prefix),
  216. })
  217. }
  218. func dup(src *net.IPNet) (dst *net.IPNet) {
  219. dst = new(net.IPNet)
  220. copy(dst.IP, src.IP)
  221. copy(dst.Mask, src.Mask)
  222. return
  223. }