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