github.com/netdata/go.d.plugin@v0.58.1/modules/dnsmasq_dhcp/parse_configuration.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package dnsmasq_dhcp
     4  
     5  import (
     6  	"bufio"
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/netdata/go.d.plugin/pkg/iprange"
    16  )
    17  
    18  func (d *DnsmasqDHCP) parseDnsmasqDHCPConfiguration() ([]iprange.Range, []net.IP) {
    19  	configs := findConfigurationFiles(d.ConfPath, d.ConfDir)
    20  
    21  	dhcpRanges := d.getDHCPRanges(configs)
    22  	dhcpHosts := d.getDHCPHosts(configs)
    23  
    24  	return dhcpRanges, dhcpHosts
    25  }
    26  
    27  func (d *DnsmasqDHCP) getDHCPRanges(configs []*configFile) []iprange.Range {
    28  	var dhcpRanges []iprange.Range
    29  	var parsed string
    30  	seen := make(map[string]bool)
    31  
    32  	for _, conf := range configs {
    33  		d.Debugf("looking in '%s'", conf.path)
    34  
    35  		for _, value := range conf.get("dhcp-range") {
    36  			d.Debugf("found dhcp-range '%s'", value)
    37  			if parsed = parseDHCPRangeValue(value); parsed == "" || seen[parsed] {
    38  				continue
    39  			}
    40  			seen[parsed] = true
    41  
    42  			r, err := iprange.ParseRange(parsed)
    43  			if r == nil || err != nil {
    44  				d.Warningf("error on parsing dhcp-range '%s', skipping it", parsed)
    45  				continue
    46  			}
    47  
    48  			d.Debugf("adding dhcp-range '%s'", parsed)
    49  			dhcpRanges = append(dhcpRanges, r)
    50  		}
    51  	}
    52  
    53  	// order: ipv4, ipv6
    54  	sort.Slice(dhcpRanges, func(i, j int) bool { return dhcpRanges[i].Family() < dhcpRanges[j].Family() })
    55  
    56  	return dhcpRanges
    57  }
    58  
    59  func (d *DnsmasqDHCP) getDHCPHosts(configs []*configFile) []net.IP {
    60  	var dhcpHosts []net.IP
    61  	seen := make(map[string]bool)
    62  	var parsed string
    63  
    64  	for _, conf := range configs {
    65  		d.Debugf("looking in '%s'", conf.path)
    66  
    67  		for _, value := range conf.get("dhcp-host") {
    68  			d.Debugf("found dhcp-host '%s'", value)
    69  			if parsed = parseDHCPHostValue(value); parsed == "" || seen[parsed] {
    70  				continue
    71  			}
    72  			seen[parsed] = true
    73  
    74  			v := net.ParseIP(parsed)
    75  			if v == nil {
    76  				d.Warningf("error on parsing dhcp-host '%s', skipping it", parsed)
    77  				continue
    78  			}
    79  
    80  			d.Debugf("adding dhcp-host '%s'", parsed)
    81  			dhcpHosts = append(dhcpHosts, v)
    82  		}
    83  	}
    84  	return dhcpHosts
    85  }
    86  
    87  /*
    88  Examples:
    89    - 192.168.0.50,192.168.0.150,12h
    90    - 192.168.0.50,192.168.0.150,255.255.255.0,12h
    91    - set:red,1.1.1.50,1.1.2.150, 255.255.252.0
    92    - 192.168.0.0,static
    93    - 1234::2,1234::500, 64, 12h
    94    - 1234::2,1234::500
    95    - 1234::2,1234::500, slaac
    96    - 1234::,ra-only
    97    - 1234::,ra-names
    98    - 1234::,ra-stateless
    99  */
   100  var reDHCPRange = regexp.MustCompile(`([0-9a-f.:]+),([0-9a-f.:]+)`)
   101  
   102  func parseDHCPRangeValue(s string) (r string) {
   103  	if strings.Contains(s, "ra-stateless") {
   104  		return
   105  	}
   106  
   107  	match := reDHCPRange.FindStringSubmatch(s)
   108  	if match == nil {
   109  		return
   110  	}
   111  
   112  	start, end := net.ParseIP(match[1]), net.ParseIP(match[2])
   113  	if start == nil || end == nil {
   114  		return
   115  	}
   116  
   117  	return fmt.Sprintf("%s-%s", start, end)
   118  }
   119  
   120  /*
   121  Examples:
   122    - 11:22:33:44:55:66,192.168.0.60
   123    - 11:22:33:44:55:66,fred,192.168.0.60,45m
   124    - 11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60
   125    - bert,192.168.0.70,infinite
   126    - id:01:02:02:04,192.168.0.60
   127    - id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61
   128    - id:marjorie,192.168.0.60
   129    - id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5]
   130  */
   131  var (
   132  	reDHCPHostV4 = regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}`)
   133  	reDHCPHostV6 = regexp.MustCompile(`\[([0-9a-f.:]+)]`)
   134  )
   135  
   136  func parseDHCPHostValue(s string) (r string) {
   137  	if strings.Contains(s, "[") {
   138  		return strings.Trim(reDHCPHostV6.FindString(s), "[]")
   139  	}
   140  	return reDHCPHostV4.FindString(s)
   141  }
   142  
   143  type (
   144  	extension string
   145  
   146  	extensions []extension
   147  
   148  	configDir struct {
   149  		path    string
   150  		include extensions
   151  		exclude extensions
   152  	}
   153  )
   154  
   155  func (e extension) match(filename string) bool {
   156  	return strings.HasSuffix(filename, string(e))
   157  }
   158  
   159  func (es extensions) match(filename string) bool {
   160  	for _, e := range es {
   161  		if e.match(filename) {
   162  			return true
   163  		}
   164  	}
   165  	return false
   166  }
   167  
   168  func parseConfDir(confDirStr string) configDir {
   169  	// # Include all the files in a directory except those ending in .bak
   170  	//#conf-dir=/etc/dnsmasq.d,.bak
   171  	//# Include all files in a directory which end in .conf
   172  	//#conf-dir=/etc/dnsmasq.d/,*.conf
   173  
   174  	parts := strings.Split(confDirStr, ",")
   175  	cd := configDir{path: parts[0]}
   176  
   177  	for _, arg := range parts[1:] {
   178  		arg = strings.TrimSpace(arg)
   179  		if strings.HasPrefix(arg, "*") {
   180  			cd.include = append(cd.include, extension(arg[1:]))
   181  		} else {
   182  			cd.exclude = append(cd.exclude, extension(arg))
   183  		}
   184  	}
   185  	return cd
   186  }
   187  
   188  func (cd configDir) isValidFilename(filename string) bool {
   189  	switch {
   190  	default:
   191  		return true
   192  	case strings.HasPrefix(filename, "."):
   193  	case strings.HasPrefix(filename, "~"):
   194  	case strings.HasPrefix(filename, "#") && strings.HasSuffix(filename, "#"):
   195  	}
   196  	return false
   197  }
   198  
   199  func (cd configDir) match(filename string) bool {
   200  	switch {
   201  	default:
   202  		return true
   203  	case !cd.isValidFilename(filename):
   204  	case len(cd.include) > 0 && !cd.include.match(filename):
   205  	case cd.exclude.match(filename):
   206  	}
   207  	return false
   208  }
   209  
   210  func (cd configDir) findConfigs() ([]string, error) {
   211  	fis, err := os.ReadDir(cd.path)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	var files []string
   217  	for _, fi := range fis {
   218  		info, err := fi.Info()
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  		if !info.Mode().IsRegular() || !cd.match(fi.Name()) {
   223  			continue
   224  		}
   225  		files = append(files, filepath.Join(cd.path, fi.Name()))
   226  	}
   227  	return files, nil
   228  }
   229  
   230  func openFile(filepath string) (f *os.File, err error) {
   231  	defer func() {
   232  		if err != nil && f != nil {
   233  			_ = f.Close()
   234  		}
   235  	}()
   236  
   237  	f, err = os.Open(filepath)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	fi, err := f.Stat()
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	if !fi.Mode().IsRegular() {
   248  		return nil, fmt.Errorf("'%s' is not a regular file", filepath)
   249  	}
   250  	return f, nil
   251  }
   252  
   253  type (
   254  	configOption struct {
   255  		key, value string
   256  	}
   257  
   258  	configFile struct {
   259  		path    string
   260  		options []configOption
   261  	}
   262  )
   263  
   264  func (cf *configFile) get(name string) []string {
   265  	var options []string
   266  	for _, o := range cf.options {
   267  		if o.key != name {
   268  			continue
   269  		}
   270  		options = append(options, o.value)
   271  	}
   272  	return options
   273  }
   274  
   275  func parseConfFile(filename string) (*configFile, error) {
   276  	f, err := openFile(filename)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	defer func() { _ = f.Close() }()
   281  
   282  	cf := configFile{path: filename}
   283  	s := bufio.NewScanner(f)
   284  	for s.Scan() {
   285  		line := strings.TrimSpace(s.Text())
   286  		if strings.HasPrefix(line, "#") {
   287  			continue
   288  		}
   289  
   290  		if !strings.Contains(line, "=") {
   291  			continue
   292  		}
   293  
   294  		line = strings.ReplaceAll(line, " ", "")
   295  		parts := strings.Split(line, "=")
   296  		if len(parts) != 2 {
   297  			continue
   298  		}
   299  
   300  		cf.options = append(cf.options, configOption{key: parts[0], value: parts[1]})
   301  	}
   302  	return &cf, nil
   303  }
   304  
   305  type ConfigFinder struct {
   306  	entryConfig    string
   307  	entryDir       string
   308  	visitedConfigs map[string]bool
   309  	visitedDirs    map[string]bool
   310  }
   311  
   312  func (f *ConfigFinder) find() []*configFile {
   313  	f.visitedConfigs = make(map[string]bool)
   314  	f.visitedDirs = make(map[string]bool)
   315  
   316  	configs := f.recursiveFind(f.entryConfig)
   317  
   318  	for _, file := range f.entryDirConfigs() {
   319  		configs = append(configs, f.recursiveFind(file)...)
   320  	}
   321  	return configs
   322  }
   323  
   324  func (f *ConfigFinder) entryDirConfigs() []string {
   325  	if f.entryDir == "" {
   326  		return nil
   327  	}
   328  	files, err := parseConfDir(f.entryDir).findConfigs()
   329  	if err != nil {
   330  		return nil
   331  	}
   332  	return files
   333  }
   334  
   335  func (f *ConfigFinder) recursiveFind(filename string) (configs []*configFile) {
   336  	if f.visitedConfigs[filename] {
   337  		return nil
   338  	}
   339  
   340  	config, err := parseConfFile(filename)
   341  	if err != nil {
   342  		return nil
   343  	}
   344  
   345  	files, dirs := config.get("conf-file"), config.get("conf-dir")
   346  
   347  	f.visitedConfigs[filename] = true
   348  	configs = append(configs, config)
   349  
   350  	for _, file := range files {
   351  		configs = append(configs, f.recursiveFind(file)...)
   352  	}
   353  
   354  	for _, dir := range dirs {
   355  		if dir == "" {
   356  			continue
   357  		}
   358  
   359  		d := parseConfDir(dir)
   360  
   361  		if f.visitedDirs[d.path] {
   362  			continue
   363  		}
   364  		f.visitedDirs[d.path] = true
   365  
   366  		files, err = d.findConfigs()
   367  		if err != nil {
   368  			continue
   369  		}
   370  
   371  		for _, file := range files {
   372  			configs = append(configs, f.recursiveFind(file)...)
   373  		}
   374  	}
   375  	return configs
   376  }
   377  
   378  func findConfigurationFiles(entryConfig string, entryDir string) []*configFile {
   379  	cf := ConfigFinder{
   380  		entryConfig: entryConfig,
   381  		entryDir:    entryDir,
   382  	}
   383  	return cf.find()
   384  }