plugin.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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 file enables static mapping of MAC <--> IP addresses.
  5. // The mapping is stored in a text file, where each mapping is described by one line containing
  6. // two fields separated by whitespace: MAC address and IP address. For example:
  7. //
  8. // $ cat leases_v4.txt
  9. // # IPv4 fixed addresses
  10. // 00:11:22:33:44:55 10.0.0.1
  11. // a1:b2:c3:d4:e5:f6 10.0.10.10 # lowercase is permitted
  12. //
  13. // $ cat leases_v6.txt
  14. // # IPv6 fixed addresses
  15. // 00:11:22:33:44:55 2001:db8::10:1
  16. // A1:B2:C3:D4:E5:F6 2001:db8::10:2
  17. //
  18. // Any text following '#' is a comment that is ignored.
  19. //
  20. // MAC addresses can be upper or lower case. IPv6 addresses should use lowercase, as per RFC-5952.
  21. //
  22. // Each MAC or IP address should normally be unique within the file. Warnings will be logged for
  23. // any duplicates.
  24. //
  25. // To specify the plugin configuration in the server6/server4 sections of the config file, just
  26. // pass the leases file name as plugin argument, e.g.:
  27. //
  28. // $ cat config.yml
  29. //
  30. // server6:
  31. // ...
  32. // plugins:
  33. // - file: "file_leases.txt" [autorefresh]
  34. // ...
  35. //
  36. // If the file path is not absolute, it is relative to the cwd where coredhcp is run.
  37. //
  38. // The optional keyword 'autorefresh' can be used as shown, or it can be omitted. When
  39. // present, the plugin will try to refresh the lease mapping during runtime whenever
  40. // the lease file is updated.
  41. //
  42. // For DHCPv4 `server4`, note that the file plugin must come after any general plugins
  43. // needed, e.g. dns or router. The order is unimportant for DHCPv6, but will affect the
  44. // order of options in the DHCPv6 response.
  45. package file
  46. import (
  47. "bufio"
  48. "errors"
  49. "fmt"
  50. "net"
  51. "net/netip"
  52. "os"
  53. "sort"
  54. "strings"
  55. "sync"
  56. "time"
  57. "unicode"
  58. "github.com/coredhcp/coredhcp/handler"
  59. "github.com/coredhcp/coredhcp/logger"
  60. "github.com/coredhcp/coredhcp/plugins"
  61. "github.com/fsnotify/fsnotify"
  62. "github.com/insomniacslk/dhcp/dhcpv4"
  63. "github.com/insomniacslk/dhcp/dhcpv6"
  64. )
  65. const (
  66. autoRefreshArg = "autorefresh"
  67. )
  68. var log = logger.GetLogger("plugins/file")
  69. // Plugin wraps plugin registration information
  70. var Plugin = plugins.Plugin{
  71. Name: "file",
  72. Setup6: setup6,
  73. Setup4: setup4,
  74. }
  75. var recLock sync.RWMutex
  76. // StaticRecords holds a MAC -> IP address mapping
  77. var StaticRecords map[string]netip.Addr
  78. // LoadDHCPv4Records loads the DHCPv4Records global map with records stored on
  79. // the specified file. The records have to be one per line, a mac address and an
  80. // IPv4 address.
  81. func LoadDHCPv4Records(filename string) (map[string]netip.Addr, error) {
  82. return loadDHCPRecords(filename, 4, netip.Addr.Is4)
  83. }
  84. // LoadDHCPv6Records loads the DHCPv6Records global map with records stored on
  85. // the specified file. The records have to be one per line, a mac address and an
  86. // IPv6 address.
  87. func LoadDHCPv6Records(filename string) (map[string]netip.Addr, error) {
  88. return loadDHCPRecords(filename, 6, netip.Addr.Is6)
  89. }
  90. // loadDHCPRecords loads the MAC<->IP mappings with records stored on
  91. // the specified file. The records have to be one per line, a mac address and an
  92. // IP address.
  93. func loadDHCPRecords(filename string, protVer int, check func(netip.Addr) bool) (map[string]netip.Addr, error) {
  94. log.Infof("reading IPv%d leases from %s", protVer, filename)
  95. addresses := make(map[string]int)
  96. f, err := os.Open(filename)
  97. if err != nil {
  98. return nil, err
  99. }
  100. defer f.Close() //nolint:errcheck // read-only open()
  101. records := make(map[string]netip.Addr)
  102. scanner := bufio.NewScanner(f)
  103. lineNo := 0
  104. for scanner.Scan() {
  105. line := strings.TrimSpace(scanner.Text())
  106. lineNo++
  107. if comment := strings.IndexRune(line, '#'); comment >= 0 {
  108. line = strings.TrimRightFunc(line[:comment], unicode.IsSpace)
  109. }
  110. if len(line) == 0 {
  111. continue
  112. }
  113. tokens := strings.Fields(line)
  114. if len(tokens) != 2 {
  115. return nil, fmt.Errorf("%s:%d malformed line, want 2 fields, got %d: %s", filename, lineNo, len(tokens), line)
  116. }
  117. hwaddr, err := net.ParseMAC(tokens[0])
  118. if err != nil {
  119. return nil, fmt.Errorf("%s:%d malformed hardware address: %s", filename, lineNo, tokens[0])
  120. }
  121. ipaddr, err := netip.ParseAddr(tokens[1])
  122. if err != nil {
  123. return nil, fmt.Errorf("%s:%d expected an IPv%d address, got: %s", filename, lineNo, protVer, tokens[1])
  124. }
  125. if !check(ipaddr) {
  126. return nil, fmt.Errorf("%s:%d expected an IPv%d address, got: %s", filename, lineNo, protVer, ipaddr)
  127. }
  128. // note that net.HardwareAddr.String() uses lowercase hexadecimal
  129. // so there's no need to convert to lowercase
  130. records[hwaddr.String()] = ipaddr
  131. addresses[strings.ToLower(tokens[0])]++
  132. addresses[strings.ToLower(tokens[1])]++
  133. }
  134. if err := scanner.Err(); err != nil {
  135. return nil, err
  136. }
  137. duplicatesWarning(addresses)
  138. return records, nil
  139. }
  140. func duplicatesWarning(ipAddresses map[string]int) {
  141. var duplicates []string
  142. for ipAddress, count := range ipAddresses {
  143. if count > 1 {
  144. duplicates = append(duplicates, fmt.Sprintf("Address %s is in %d records", ipAddress, count))
  145. }
  146. }
  147. sort.Strings(duplicates)
  148. for _, warning := range duplicates {
  149. log.Warning(warning)
  150. }
  151. }
  152. // Handler6 handles DHCPv6 packets for the file plugin
  153. func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
  154. m, err := req.GetInnerMessage()
  155. if err != nil {
  156. log.Errorf("BUG: could not decapsulate: %v", err)
  157. return nil, true
  158. }
  159. if m.Options.OneIANA() == nil {
  160. log.Debug("No address requested")
  161. return resp, false
  162. }
  163. mac, err := dhcpv6.ExtractMAC(req)
  164. if err != nil {
  165. log.Warningf("Could not find client MAC, passing")
  166. return resp, false
  167. }
  168. log.Debugf("looking up an IP address for MAC %s", mac.String())
  169. recLock.RLock()
  170. defer recLock.RUnlock()
  171. ipaddr, ok := StaticRecords[mac.String()]
  172. if !ok {
  173. log.Warningf("MAC address %s is unknown", mac.String())
  174. return resp, false
  175. }
  176. log.Debugf("found IP address %s for MAC %s", ipaddr, mac.String())
  177. resp.AddOption(&dhcpv6.OptIANA{
  178. IaId: m.Options.OneIANA().IaId,
  179. Options: dhcpv6.IdentityOptions{Options: []dhcpv6.Option{
  180. &dhcpv6.OptIAAddress{
  181. IPv6Addr: ipaddr.AsSlice(),
  182. PreferredLifetime: 3600 * time.Second,
  183. ValidLifetime: 3600 * time.Second,
  184. },
  185. }},
  186. })
  187. return resp, false
  188. }
  189. // Handler4 handles DHCPv4 packets for the file plugin
  190. func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
  191. recLock.RLock()
  192. defer recLock.RUnlock()
  193. ipaddr, ok := StaticRecords[req.ClientHWAddr.String()]
  194. if !ok {
  195. log.Warningf("MAC address %s is unknown", req.ClientHWAddr.String())
  196. return resp, false
  197. }
  198. log.Debugf("found IP address %s for MAC %s", ipaddr, req.ClientHWAddr.String())
  199. resp.YourIPAddr = ipaddr.AsSlice()
  200. return resp, true
  201. }
  202. func setup6(args ...string) (handler.Handler6, error) {
  203. h6, _, err := setupFile(true, args...)
  204. return h6, err
  205. }
  206. func setup4(args ...string) (handler.Handler4, error) {
  207. _, h4, err := setupFile(false, args...)
  208. return h4, err
  209. }
  210. func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
  211. var err error
  212. if len(args) < 1 {
  213. return nil, nil, errors.New("need a file name")
  214. }
  215. filename := args[0]
  216. if filename == "" {
  217. return nil, nil, errors.New("got empty file name")
  218. }
  219. // load initial database from lease file
  220. if err = loadFromFile(v6, filename); err != nil {
  221. return nil, nil, err
  222. }
  223. // when the 'autorefresh' argument was passed, watch the lease file for
  224. // changes and reload the lease mapping on any event
  225. if len(args) > 1 && args[1] == autoRefreshArg {
  226. // creates a new file watcher
  227. watcher, err := fsnotify.NewWatcher()
  228. if err != nil {
  229. return nil, nil, fmt.Errorf("failed to create watcher: %w", err)
  230. }
  231. // have file watcher watch over lease file
  232. if err = watcher.Add(filename); err != nil {
  233. return nil, nil, fmt.Errorf("failed to watch %s: %w", filename, err)
  234. }
  235. // very simple watcher on the lease file to trigger a refresh on any event
  236. // on the file
  237. go func() {
  238. for range watcher.Events {
  239. err := loadFromFile(v6, filename)
  240. if err != nil {
  241. log.Warningf("failed to refresh from %s: %s", filename, err)
  242. continue
  243. }
  244. log.Infof("updated to %d leases from %s", len(StaticRecords), filename)
  245. }
  246. }()
  247. }
  248. log.Infof("loaded %d leases from %s", len(StaticRecords), filename)
  249. return Handler6, Handler4, nil
  250. }
  251. func loadFromFile(v6 bool, filename string) error {
  252. var err error
  253. var records map[string]netip.Addr
  254. var protver int
  255. if v6 {
  256. protver = 6
  257. records, err = LoadDHCPv6Records(filename)
  258. } else {
  259. protver = 4
  260. records, err = LoadDHCPv4Records(filename)
  261. }
  262. if err != nil {
  263. return fmt.Errorf("failed to load DHCPv%d records: %w", protver, err)
  264. }
  265. recLock.Lock()
  266. defer recLock.Unlock()
  267. StaticRecords = records
  268. return nil
  269. }