From 9c1665a7599d014fe7bc4f307f04352ac8fcaeba Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 5 Jan 2024 23:39:31 +0700 Subject: [PATCH] internal/clientinfo: add kea-dhcp4 parser --- client_info.go | 1 + internal/clientinfo/dhcp.go | 59 ++++++++++++++++++++++++- internal/clientinfo/dhcp_lease_files.go | 1 + internal/clientinfo/dhcp_test.go | 41 +++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/client_info.go b/client_info.go index 05d2910..24e09c3 100644 --- a/client_info.go +++ b/client_info.go @@ -18,4 +18,5 @@ type LeaseFileFormat string const ( Dnsmasq LeaseFileFormat = "dnsmasq" IscDhcpd LeaseFileFormat = "isc-dhcpd" + KeaDHCP4 LeaseFileFormat = "kea-dhcp4" ) diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index ebbeb77..1bbe405 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -3,6 +3,7 @@ package clientinfo import ( "bufio" "bytes" + "encoding/csv" "fmt" "io" "net" @@ -202,7 +203,8 @@ func (d *dhcp) dnsmasqReadClientInfoFile(name string) error { } -// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file. +// dnsmasqReadClientInfoReader performs the same task as dnsmasqReadClientInfoFile, +// but by reading from an io.Reader instead of file. func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error { return lineread.Reader(reader, func(line []byte) error { fields := bytes.Fields(line) @@ -244,7 +246,8 @@ func (d *dhcp) iscDHCPReadClientInfoFile(name string) error { return d.iscDHCPReadClientInfoReader(f) } -// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file. +// iscDHCPReadClientInfoReader performs the same task as iscDHCPReadClientInfoFile, +// but by reading from an io.Reader instead of file. func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error { s := bufio.NewScanner(reader) var ip, mac, hostname string @@ -287,6 +290,58 @@ func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error { return nil } +// keaDhcp4ReadClientInfoFile populates dhcp table with client info reading from kea dhcp4 lease file. +func (d *dhcp) keaDhcp4ReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return d.keaDhcp4ReadClientInfoReader(bufio.NewReader(f)) + +} + +// keaDhcp4ReadClientInfoReader performs the same task as keaDhcp4ReadClientInfoFile, +// but by reading from an io.Reader instead of file. +func (d *dhcp) keaDhcp4ReadClientInfoReader(r io.Reader) error { + cr := csv.NewReader(r) + for { + record, err := cr.Read() + if err == io.EOF { + break + } + if err != nil { + return err + } + if len(record) < 9 { + continue // hostname is at 9th field, so skipping record with not enough fields. + } + if record[0] == "address" { + continue // skip header. + } + mac := record[1] + if _, err := net.ParseMAC(mac); err != nil { // skip invalid MAC + continue + } + ip := normalizeIP(record[0]) + if net.ParseIP(ip) == nil { + ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip) + ip = "" + } + + d.mac.Store(ip, mac) + d.ip.Store(mac, ip) + hostname := record[8] + if hostname == "*" { + continue + } + name := normalizeHostname(hostname) + d.mac2name.Store(mac, name) + d.ip2name.Store(ip, name) + } + return nil +} + // addSelf populates current host info to dhcp, so queries from // the host itself can be attached with proper client info. func (d *dhcp) addSelf() { diff --git a/internal/clientinfo/dhcp_lease_files.go b/internal/clientinfo/dhcp_lease_files.go index 4932a4b..1b5d829 100644 --- a/internal/clientinfo/dhcp_lease_files.go +++ b/internal/clientinfo/dhcp_lease_files.go @@ -15,4 +15,5 @@ var clientInfoFiles = map[string]ctrld.LeaseFileFormat{ "/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla + "/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense } diff --git a/internal/clientinfo/dhcp_test.go b/internal/clientinfo/dhcp_test.go index 359f441..07dbf5a 100644 --- a/internal/clientinfo/dhcp_test.go +++ b/internal/clientinfo/dhcp_test.go @@ -67,6 +67,41 @@ lease 192.168.1.2 { "00:00:00:00:00:04", "example", }, + { + "kea-dhcp4 good", + `address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id +192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0 +`, + d.keaDhcp4ReadClientInfoReader, + "00:00:00:00:00:05", + "foo", + }, + { + "kea-dhcp4 no-header", + `192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0`, + d.keaDhcp4ReadClientInfoReader, + "00:00:00:00:00:05", + "foo", + }, + { + "kea-dhcp4 hostname *", + `address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id +192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,*,0,,0 +`, + d.keaDhcp4ReadClientInfoReader, + "00:00:00:00:00:05", + "*", + }, + { + "kea-dhcp4 bad", + `address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id +192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0 +192.168.0.124,invalid_MAC,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0 +`, + d.keaDhcp4ReadClientInfoReader, + "00:00:00:00:00:05", + "foo", + }, } for _, tc := range tests { @@ -76,6 +111,12 @@ lease 192.168.1.2 { t.Errorf("readClientInfoReader() error = %v", err) } val, existed := d.mac2name.Load(tc.mac) + if tc.hostname == "*" { + if existed { + t.Errorf("* hostname must be skipped") + } + return + } if !existed { t.Error("client info missing") }