plugin.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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.Infof("Could not find client MAC for %s, passing", req)
  166. return resp, false
  167. }
  168. recLock.RLock()
  169. defer recLock.RUnlock()
  170. ipaddr, ok := StaticRecords[mac.String()]
  171. if !ok {
  172. log.Infof("MAC address %s is unknown", mac)
  173. return resp, false
  174. }
  175. log.Infof("MAC address %s given IP address %s", mac, ipaddr)
  176. resp.AddOption(&dhcpv6.OptIANA{
  177. IaId: m.Options.OneIANA().IaId,
  178. Options: dhcpv6.IdentityOptions{Options: []dhcpv6.Option{
  179. &dhcpv6.OptIAAddress{
  180. IPv6Addr: ipaddr.AsSlice(),
  181. PreferredLifetime: 3600 * time.Second,
  182. ValidLifetime: 3600 * time.Second,
  183. },
  184. }},
  185. })
  186. return resp, false
  187. }
  188. // Handler4 handles DHCPv4 packets for the file plugin
  189. func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
  190. recLock.RLock()
  191. defer recLock.RUnlock()
  192. ipaddr, ok := StaticRecords[req.ClientHWAddr.String()]
  193. if !ok {
  194. log.Infof("MAC address %s is unknown", req.ClientHWAddr)
  195. return resp, false
  196. }
  197. resp.YourIPAddr = ipaddr.AsSlice()
  198. log.Infof("MAC address %s given IP address %s", req.ClientHWAddr, ipaddr)
  199. return resp, true
  200. }
  201. func setup6(args ...string) (handler.Handler6, error) {
  202. h6, _, err := setupFile(true, args...)
  203. return h6, err
  204. }
  205. func setup4(args ...string) (handler.Handler4, error) {
  206. _, h4, err := setupFile(false, args...)
  207. return h4, err
  208. }
  209. func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
  210. var err error
  211. if len(args) < 1 {
  212. return nil, nil, errors.New("need a file name")
  213. }
  214. filename := args[0]
  215. if filename == "" {
  216. return nil, nil, errors.New("got empty file name")
  217. }
  218. // load initial database from lease file
  219. if err = loadFromFile(v6, filename); err != nil {
  220. return nil, nil, err
  221. }
  222. // when the 'autorefresh' argument was passed, watch the lease file for
  223. // changes and reload the lease mapping on any event
  224. if len(args) > 1 && args[1] == autoRefreshArg {
  225. // creates a new file watcher
  226. watcher, err := fsnotify.NewWatcher()
  227. if err != nil {
  228. return nil, nil, fmt.Errorf("failed to create watcher: %w", err)
  229. }
  230. // have file watcher watch over lease file
  231. if err = watcher.Add(filename); err != nil {
  232. return nil, nil, fmt.Errorf("failed to watch %s: %w", filename, err)
  233. }
  234. // very simple watcher on the lease file to trigger a refresh on any event
  235. // on the file
  236. go func() {
  237. for range watcher.Events {
  238. err := loadFromFile(v6, filename)
  239. if err != nil {
  240. log.Warningf("failed to refresh from %s: %s", filename, err)
  241. continue
  242. }
  243. log.Infof("updated to %d leases from %s", len(StaticRecords), filename)
  244. }
  245. }()
  246. }
  247. log.Infof("loaded %d leases from %s", len(StaticRecords), filename)
  248. return Handler6, Handler4, nil
  249. }
  250. func loadFromFile(v6 bool, filename string) error {
  251. var err error
  252. var records map[string]netip.Addr
  253. var protver int
  254. if v6 {
  255. protver = 6
  256. records, err = LoadDHCPv6Records(filename)
  257. } else {
  258. protver = 4
  259. records, err = LoadDHCPv4Records(filename)
  260. }
  261. if err != nil {
  262. return fmt.Errorf("failed to load DHCPv%d records: %w", protver, err)
  263. }
  264. recLock.Lock()
  265. defer recLock.Unlock()
  266. StaticRecords = records
  267. return nil
  268. }