diff --git a/internal/config/config.go b/internal/config/config.go index d313559..e499ca5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net/netip" "os" ) @@ -13,9 +14,39 @@ type HealthchecksConfig struct { PingKey string `json:"ping_key"` } +type DNSMasqConfig struct { + DHCPRangeStart netip.Addr + DHCPRangeEnd netip.Addr + LeasesPath string +} + type Config struct { HealthchecksConfig `json:"healthchecks"` - LeasesPath string `json:"leases_path"` + DNSMasqConfigFile string `json:"dnsmasq_config"` + DNSMasqConfig +} + +func (c *Config) parseDNSMasqConf() error { + dnsMasqRootContent, err := os.ReadFile(c.DNSMasqConfigFile) + if err != nil { + return fmt.Errorf("failed to read dnsmasq config: %w", err) + } + + dnsmasqRawConf, err := parseDNSMasqOptions(string(dnsMasqRootContent)) + if err != nil { + return fmt.Errorf("failed to parse dnsmasq config: %w", err) + } + c.LeasesPath = dnsmasqRawConf.leaseFile + c.DHCPRangeStart, err = netip.ParseAddr(dnsmasqRawConf.rangeStart) + if err != nil { + return fmt.Errorf("failed to parse dhcp range start: %w", err) + } + c.DHCPRangeEnd, err = netip.ParseAddr(dnsmasqRawConf.rangeEnd) + if err != nil { + return fmt.Errorf("failed to parse dhcp range end: %w", err) + } + + return nil } func defaultConfig() *Config { @@ -23,7 +54,7 @@ func defaultConfig() *Config { HealthchecksConfig: HealthchecksConfig{ Enabled: false, }, - LeasesPath: "/var/lib/misc/dnsmasq.leases", + DNSMasqConfigFile: "etc/dnsmasq.conf", } } @@ -31,7 +62,8 @@ func New(path string) (*Config, error) { fileContent, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { - return defaultConfig(), nil + conf := defaultConfig() + return conf, conf.parseDNSMasqConf() } return nil, fmt.Errorf("failed to read config file %w", err) } @@ -40,5 +72,5 @@ func New(path string) (*Config, error) { if err := json.Unmarshal(fileContent, &c); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } - return &c, nil + return &c, c.parseDNSMasqConf() } diff --git a/internal/config/dnsmasq.go b/internal/config/dnsmasq.go new file mode 100644 index 0000000..2d08622 --- /dev/null +++ b/internal/config/dnsmasq.go @@ -0,0 +1,96 @@ +package config + +import ( + "fmt" + "os" + "path" + "strings" +) + +const ( + leaseFileKey = "dhcp-leasefile" + rangeKey = "dhcp-range" + confFileKey = "conf-file" + confDirKey = "conf-dir" +) + +func tryParseLineOption(line, key string) (string, bool) { + if !strings.HasPrefix(line, key) { + return "", false + } + + _, val, found := strings.Cut(line, "=") + return val, found +} + +type dnsmasqConfOptions struct { + leaseFile string + rangeStart string + rangeEnd string +} + +func defaultDNSMasqOptions() map[string]string { + return map[string]string{ + confDirKey: "/etc/dnsmasq.d", + confFileKey: "", + leaseFileKey: "/var/lib/misc/dnsmasq.leases", + rangeKey: "", + } +} + +func parseDNSMasqConfFile(content string, options map[string]string) { + for _, l := range strings.Split(content, "\n") { + for key := range options { + newVal, found := tryParseLineOption(l, key) + if found { + options[key] = newVal + } + } + } +} + +func parseDNSMasqOptions(rootFileContent string) (dnsmasqConfOptions, error) { + rawOptions := defaultDNSMasqOptions() + + // start with parsing the root file + parseDNSMasqConfFile(rootFileContent, rawOptions) + + // get all additional conf files in the provided directory + confDir := rawOptions[confDirKey] + if confDir != "" { + entries, err := os.ReadDir(confDir) + if err != nil { + return dnsmasqConfOptions{}, fmt.Errorf("failed to get list of additional config files: %w", err) + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".conf") { + continue + } + fileContent, err := os.ReadFile(path.Join(confDir, e.Name())) + if err != nil { + return dnsmasqConfOptions{}, fmt.Errorf("failed to read sub config file: %w", err) + } + parseDNSMasqConfFile(string(fileContent), rawOptions) + } + } + + // get any additional config file + if additionalFile := rawOptions[confFileKey]; additionalFile != "" { + filecontent, err := os.ReadFile(additionalFile) + if err != nil { + return dnsmasqConfOptions{}, fmt.Errorf("failed to read additional config file: %w", err) + } + parseDNSMasqConfFile(string(filecontent), rawOptions) + } + + dhcpRange := strings.Split(rawOptions[rangeKey], ",") + if len(dhcpRange) < 2 { + return dnsmasqConfOptions{}, fmt.Errorf("invalid value for DHCP range: %s", rawOptions[rangeKey]) + } + + return dnsmasqConfOptions{ + leaseFile: rawOptions[leaseFileKey], + rangeStart: dhcpRange[0], + rangeEnd: dhcpRange[1], + }, nil +} diff --git a/internal/dnsmasq/dnsmasq.go b/internal/dnsmasq/dnsmasq.go index 71f3fc5..5c3b4d6 100644 --- a/internal/dnsmasq/dnsmasq.go +++ b/internal/dnsmasq/dnsmasq.go @@ -2,6 +2,7 @@ package dnsmasq import ( "fmt" + "net/netip" "os" "strconv" "strings" @@ -11,7 +12,7 @@ import ( type Lease struct { ExpireDate time.Time Mac string - IP string + IP netip.Addr Hostname string ClientID string } @@ -31,10 +32,15 @@ func parseLeaseLine(rawLine string) (*Lease, error) { return nil, fmt.Errorf("unexpected unix timestamp value %s", lineValues[0]) } + ip, err := netip.ParseAddr(lineValues[2]) + if err != nil { + return nil, fmt.Errorf("unexpected IP address: %s: %w", lineValues[2], err) + } + return &Lease{ ExpireDate: time.Unix(int64(expireTimeInt), 0), Mac: lineValues[1], - IP: lineValues[2], + IP: ip, Hostname: lineValues[3], ClientID: lineValues[4], }, nil @@ -59,3 +65,19 @@ func GetLeases(leasesPath string) ([]*Lease, error) { } return leases, nil } + +func GetDynamicLeases(leasesPath string, dhcpStart, dhcpEnd netip.Addr) ([]*Lease, error) { + allLeases, err := GetLeases(leasesPath) + if err != nil { + return nil, err + } + + filteredLeases := []*Lease{} + for _, l := range allLeases { + if l.IP.Less(dhcpEnd) && dhcpStart.Less(l.IP) { + filteredLeases = append(filteredLeases, l) + } + } + + return filteredLeases, nil +} diff --git a/main.go b/main.go index 342d28f..cd00874 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,7 @@ func main() { fmt.Fprintf(os.Stderr, "Failed to notify process start: %s\n", err) } - leases, err := dnsmasq.GetLeases(conf.LeasesPath) + leases, err := dnsmasq.GetDynamicLeases(conf.LeasesPath, conf.DHCPRangeStart, conf.DHCPRangeEnd) if err != nil { fmt.Fprintf(os.Stderr, "Failed to parse leases: %s\n", err) if err := healthchecks.Failure(context.Background(), conf.HealthchecksConfig, healtchecksClt, check, "failed to parse lease file"); err != nil {