From 60fa3b2e95e5fc111884f13c8bfa465f030a0b40 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 28 Feb 2019 18:24:01 +0300 Subject: [PATCH 01/14] * dhcp: refactor; log client's HW addr --- dhcpd/dhcpd.go | 65 ++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 05d753b8..7cd3affc 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -126,6 +126,7 @@ func (s *Server) Start(config *ServerConfig) error { if err != nil { return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") } + log.Info("DHCP: listening on 0.0.0.0:67") s.conn = c @@ -213,7 +214,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { var foundIP net.IP for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ { newIP := dhcp4.IPAdd(s.leaseStart, i) - foundHWaddr := s.getIPpool(newIP) + foundHWaddr := s.findReservedHWaddr(newIP) log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr) if foundHWaddr != nil && len(foundHWaddr) != 0 { // if !bytes.Equal(foundHWaddr, hwaddr) { @@ -236,7 +237,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { return foundIP, nil } -func (s *Server) getIPpool(ip net.IP) net.HardwareAddr { +func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr { rawIP := []byte(ip) IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} return s.IPpool[IP4] @@ -256,33 +257,22 @@ func (s *Server) unreserveIP(ip net.IP) { // ServeDHCP handles an incoming DHCP request func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Got %v message", msgType) + log.Tracef("Message from client %s: %d", p.CHAddr(), msgType) + log.Tracef("Leases:") for i, lease := range s.leases { log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry) } - log.Tracef("IP pool:") - for ip, hwaddr := range s.IPpool { - log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr) - } switch msgType { case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? - // find a lease, but don't update lease time - log.Tracef("Got from client: Discover") - lease, err := s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil - } - reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) - return reply + return s.handleDiscover(p, options) + case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals) // start/renew a lease -- update lease time // some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request return s.handleDHCP4Request(p, options) + case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP log.Tracef("Got from client: Decline") @@ -291,25 +281,43 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it log.Tracef("Got from client: Inform") - // do nothing // from server -- ignore those but enumerate just in case case dhcp4.Offer: // Broadcast From Server - Here's an IP - log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer") + log.Printf("DHCP: received message from another server: Offer") + case dhcp4.ACK: // From Server, Yes you can have that IP - log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK") + log.Printf("DHCP: received message from another server: ACK") + case dhcp4.NAK: // From Server, No you cannot have that IP - log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK") + log.Printf("DHCP: received message from another server: NAK") + default: - log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType) + log.Printf("DHCP: unknown packet %v from client %s", msgType, p.CHAddr()) return nil } return nil } +func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { + // find a lease, but don't update lease time + + lease, err := s.reserveLease(p) + if err != nil { + log.Tracef("Couldn't find free lease: %s", err) + // couldn't find lease, don't respond + return nil + } + + opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) + reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt) + log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) + return reply +} + func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Got from client: Request") - if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) { + server := options[dhcp4.OptionServerIdentifier] + if server != nil && !net.IP(server).Equal(s.ipnet.IP) { log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) return nil // Message not for this dhcp server } @@ -341,7 +349,8 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 // IP matches lease IP, nothing else to do lease.Expiry = time.Now().Add(s.leaseTime) log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) + opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) + return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt) } // @@ -350,7 +359,7 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP) - hwaddr := s.getIPpool(reqIP) + hwaddr := s.findReservedHWaddr(reqIP) if hwaddr == nil { // not in pool, check if it's in DHCP range if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) { @@ -379,7 +388,7 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) } -// Leases returns the list of current DHCP leases +// Leases returns the list of current DHCP leases (thread-safe) func (s *Server) Leases() []*Lease { s.RLock() result := s.leases From 08bf9b0acb7bc7ce73f53a8f40e8c3f23e2e3ba2 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 1 Mar 2019 14:48:33 +0300 Subject: [PATCH 02/14] * dhcp: remove code which forces an update of current lease's IP in Request message handler --- dhcpd/dhcpd.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 7cd3affc..d6f1c528 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -358,33 +358,6 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 // log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP) - - hwaddr := s.findReservedHWaddr(reqIP) - if hwaddr == nil { - // not in pool, check if it's in DHCP range - if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) { - // okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP - log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr()) - s.unreserveIP(lease.IP) - lease.IP = reqIP - s.reserveIP(reqIP, p.CHAddr()) - lease.Expiry = time.Now().Add(s.leaseTime) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - } - } - - if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) { - log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr) - } - - // requsted IP is not sufficient, reply with NAK - if hwaddr != nil { - log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - // requested IP is outside of DHCP range - log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr()) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) } From e164cff02b0f20a03c0c3050eb8f6e4a812f461b Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 1 Mar 2019 20:03:22 +0300 Subject: [PATCH 03/14] + dhcp: test --- dhcpd/dhcpd.go | 8 +++ dhcpd/dhcpd_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 dhcpd/dhcpd_test.go diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index d6f1c528..b9affb5a 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -368,3 +368,11 @@ func (s *Server) Leases() []*Lease { s.RUnlock() return result } + +// Reset internal state +func (s *Server) reset() { + s.Lock() + s.leases = nil + s.Unlock() + s.IPpool = make(map[[4]byte]net.HardwareAddr) +} diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go new file mode 100644 index 00000000..10ca1b59 --- /dev/null +++ b/dhcpd/dhcpd_test.go @@ -0,0 +1,115 @@ +package dhcpd + +import ( + "bytes" + "net" + "testing" + "time" + + "github.com/krolaw/dhcp4" +) + +func check(t *testing.T, result bool, msg string) { + if !result { + t.Fatal(msg) + } +} + +// Tests performed: +// . Handle Discover message (lease reserve) +// . Handle Request message (lease commit) +func TestDHCP(t *testing.T) { + var s = Server{} + var p, p2 dhcp4.Packet + var hw net.HardwareAddr + var lease *Lease + var opt dhcp4.Options + + s.leaseStart = []byte{1, 1, 1, 1} + s.leaseStop = []byte{1, 1, 1, 2} + s.leaseTime = 5 * time.Second + s.leaseOptions = dhcp4.Options{} + s.ipnet = &net.IPNet{ + IP: []byte{1, 2, 3, 4}, + Mask: []byte{0xff, 0xff, 0xff, 0xff}, + } + + p = make(dhcp4.Packet, 241) + + // Reserve an IP + hw = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + lease, _ = s.reserveLease(p) + check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") + check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") + + // Try to reserve another IP for the same machine - no new IP must be reserved + hw = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + lease, _ = s.reserveLease(p) + check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") + check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") + + // Reserve an IP - the next IP from the range + hw = []byte{2, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + lease, _ = s.reserveLease(p) + check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") + check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 2}), "lease.IP") + + // Reserve an IP - we have no more available IPs + p.SetCHAddr([]byte{3, 2, 3, 4, 5, 6}) + lease, _ = s.reserveLease(p) + check(t, lease == nil, "lease == nil") + + // Decline request for a lease which doesn't match our internal state + hw = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + p.SetCIAddr([]byte{0, 0, 0, 0}) + opt = make(dhcp4.Options, 10) + // ask a different IP + opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2} + p2 = s.handleDHCP4Request(p, opt) + opt = p2.ParseOptions() + check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK") + + // Commit the previously reserved lease + hw = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + p.SetCIAddr([]byte{0, 0, 0, 0}) + opt = make(dhcp4.Options, 10) + opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} + p2 = s.handleDHCP4Request(p, opt) + opt = p2.ParseOptions() + check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK") + check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr") + check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr") + check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") + check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") + + s.reset() + misc(t, &s) +} + +// Small tests that don't require a static server's state +func misc(t *testing.T, s *Server) { + var p, p2 dhcp4.Packet + var hw net.HardwareAddr + var opt dhcp4.Options + + p = make(dhcp4.Packet, 241) + + // Commit a lease for an IP without prior Discover-Offer packets + hw = []byte{2, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + p.SetCIAddr([]byte{0, 0, 0, 0}) + opt = make(dhcp4.Options, 10) + opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} + p2 = s.handleDHCP4Request(p, opt) + opt = p2.ParseOptions() + check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK") + check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr") + check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr") + check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") + check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") +} From a2e9d6945200bce5f270fbe95dedd65a0d08b5e4 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 6 Mar 2019 12:20:34 +0300 Subject: [PATCH 04/14] * use golibs v0.1.1: file.SafeWrite() --- config.go | 3 ++- filter.go | 3 ++- go.mod | 2 +- go.sum | 3 ++- helpers.go | 21 --------------------- upgrade.go | 3 ++- 6 files changed, 9 insertions(+), 26 deletions(-) diff --git a/config.go b/config.go index b0da6532..1c12f1c6 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" ) @@ -217,7 +218,7 @@ func (c *configuration) write() error { log.Error("Couldn't generate YAML file: %s", err) return err } - err = safeWriteFile(configFile, yamlText) + err = file.SafeWrite(configFile, yamlText) if err != nil { log.Error("Couldn't save YAML config: %s", err) return err diff --git a/filter.go b/filter.go index 280c403f..6b61d504 100644 --- a/filter.go +++ b/filter.go @@ -12,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" ) @@ -220,7 +221,7 @@ func (filter *filter) save() error { log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath) body := []byte(strings.Join(filter.Rules, "\n")) - err := safeWriteFile(filterFilePath, body) + err := file.SafeWrite(filterFilePath, body) // update LastUpdated field after saving the file filter.LastUpdated = filter.LastTimeUpdated() diff --git a/go.mod b/go.mod index 589d0982..2f0dd5bd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.12 require ( github.com/AdguardTeam/dnsproxy v0.11.2 - github.com/AdguardTeam/golibs v0.1.0 + github.com/AdguardTeam/golibs v0.1.1 github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 github.com/go-ole/go-ole v1.2.1 // indirect diff --git a/go.sum b/go.sum index 6e4a79f1..51c15f76 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,8 @@ github.com/AdguardTeam/dnsproxy v0.11.2 h1:S/Ag2q9qoZsmW1fvMohPZP7/5amEtz8NmFCp8kxUalQ= github.com/AdguardTeam/dnsproxy v0.11.2/go.mod h1:EPp92b5cYR7HZpO+OQu6xC7AyhUoBaXW3sfa3exq/0I= -github.com/AdguardTeam/golibs v0.1.0 h1:Mo1QNKC8eSbqczhxfdBXYCrUMwvgCyCwZFyWv+2Gdng= github.com/AdguardTeam/golibs v0.1.0/go.mod h1:zhi6xGwK4cMpjDocybhhLgvcGkstiSIjlpKbvyxC5Yc= +github.com/AdguardTeam/golibs v0.1.1 h1:aepIN7yulf8I4Ub2c0cAaIizfSHPVXB2wrh8j4BJxl4= +github.com/AdguardTeam/golibs v0.1.1/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= diff --git a/helpers.go b/helpers.go index 8e884d36..25e35205 100644 --- a/helpers.go +++ b/helpers.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "net/url" @@ -19,26 +18,6 @@ import ( "github.com/joomcode/errorx" ) -// ---------------------------------- -// helper functions for working with files -// ---------------------------------- - -// Writes data first to a temporary file and then renames it to what's specified in path -func safeWriteFile(path string, data []byte) error { - dir := filepath.Dir(path) - err := os.MkdirAll(dir, 0755) - if err != nil { - return err - } - - tmpPath := path + ".tmp" - err = ioutil.WriteFile(tmpPath, data, 0644) - if err != nil { - return err - } - return os.Rename(tmpPath, path) -} - // ---------------------------------- // helper functions for HTTP handlers // ---------------------------------- diff --git a/upgrade.go b/upgrade.go index 0c9aeca7..e730d34b 100644 --- a/upgrade.go +++ b/upgrade.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" ) @@ -86,7 +87,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err return err } - err = safeWriteFile(configFile, body) + err = file.SafeWrite(configFile, body) if err != nil { log.Printf("Couldn't save YAML config: %s", err) return err From 67014c40f70e7afa1f4aaa064324194836f06086 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 5 Mar 2019 17:15:38 +0300 Subject: [PATCH 05/14] + DHCP: On-disk database for lease table --- dhcpd/db.go | 98 +++++++++++++++++++++++++++++++++++++++++++++ dhcpd/dhcpd.go | 3 ++ dhcpd/dhcpd_test.go | 45 +++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 dhcpd/db.go diff --git a/dhcpd/db.go b/dhcpd/db.go new file mode 100644 index 00000000..1caa7d81 --- /dev/null +++ b/dhcpd/db.go @@ -0,0 +1,98 @@ +// On-disk database for lease table + +package dhcpd + +import ( + "encoding/json" + "io/ioutil" + "net" + "os" + "time" + + "github.com/AdguardTeam/golibs/file" + "github.com/AdguardTeam/golibs/log" + "github.com/krolaw/dhcp4" +) + +const dbFilename = "leases.db" + +type leaseJSON struct { + HWAddr []byte `json:"mac"` + IP []byte `json:"ip"` + Hostname string `json:"host"` + Expiry int64 `json:"exp"` +} + +// Load lease table from DB +func (s *Server) dbLoad() { + data, err := ioutil.ReadFile(dbFilename) + if err != nil { + if !os.IsNotExist(err) { + log.Error("DHCP: can't read file %s: %v", dbFilename, err) + } + return + } + + obj := []leaseJSON{} + err = json.Unmarshal(data, &obj) + if err != nil { + log.Error("DHCP: invalid DB: %v", err) + return + } + + s.leases = nil + s.IPpool = make(map[[4]byte]net.HardwareAddr) + + numLeases := len(obj) + for i := range obj { + + if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) { + log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP) + continue + } + + lease := Lease{ + HWAddr: obj[i].HWAddr, + IP: obj[i].IP, + Hostname: obj[i].Hostname, + Expiry: time.Unix(obj[i].Expiry, 0), + } + + s.leases = append(s.leases, &lease) + + s.reserveIP(lease.IP, lease.HWAddr) + } + log.Info("DHCP: loaded %d leases from DB", numLeases) +} + +// Store lease table in DB +func (s *Server) dbStore() { + var leases []leaseJSON + + for i := range s.leases { + if s.leases[i].Expiry.Unix() == 0 { + continue + } + lease := leaseJSON{ + HWAddr: s.leases[i].HWAddr, + IP: s.leases[i].IP, + Hostname: s.leases[i].Hostname, + Expiry: s.leases[i].Expiry.Unix(), + } + leases = append(leases, lease) + } + + data, err := json.Marshal(leases) + if err != nil { + log.Error("json.Marshal: %v", err) + return + } + + err = file.SafeWrite(dbFilename, data) + if err != nil { + log.Error("DHCP: can't store lease table on disk: %v filename: %s", + err, dbFilename) + return + } + log.Info("DHCP: stored %d leases in DB", len(leases)) +} diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index b9affb5a..3920bcef 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -122,6 +122,8 @@ func (s *Server) Start(config *ServerConfig) error { s.closeConn() } + s.dbLoad() + c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets if err != nil { return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") @@ -153,6 +155,7 @@ func (s *Server) Stop() error { return wrapErrPrint(err, "Couldn't close UDP listening socket") } + s.dbStore() return nil } diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go index 10ca1b59..2d675ed9 100644 --- a/dhcpd/dhcpd_test.go +++ b/dhcpd/dhcpd_test.go @@ -3,6 +3,7 @@ package dhcpd import ( "bytes" "net" + "os" "testing" "time" @@ -113,3 +114,47 @@ func misc(t *testing.T, s *Server) { check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") } + +// Leases database store/load +func TestDB(t *testing.T) { + var s = Server{} + var p dhcp4.Packet + var hw1, hw2 net.HardwareAddr + var lease *Lease + + s.leaseStart = []byte{1, 1, 1, 1} + s.leaseStop = []byte{1, 1, 1, 2} + s.leaseTime = 5 * time.Second + s.leaseOptions = dhcp4.Options{} + s.ipnet = &net.IPNet{ + IP: []byte{1, 2, 3, 4}, + Mask: []byte{0xff, 0xff, 0xff, 0xff}, + } + + p = make(dhcp4.Packet, 241) + + hw1 = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw1) + lease, _ = s.reserveLease(p) + lease.Expiry = time.Unix(4000000001, 0) + + hw2 = []byte{2, 2, 3, 4, 5, 6} + p.SetCHAddr(hw2) + lease, _ = s.reserveLease(p) + lease.Expiry = time.Unix(4000000002, 0) + + os.Remove("leases.db") + s.dbStore() + s.reset() + + s.dbLoad() + check(t, bytes.Equal(s.leases[0].HWAddr, hw1), "leases[0].HWAddr") + check(t, bytes.Equal(s.leases[0].IP, []byte{1, 1, 1, 1}), "leases[0].IP") + check(t, s.leases[0].Expiry.Unix() == 4000000001, "leases[0].Expiry") + + check(t, bytes.Equal(s.leases[1].HWAddr, hw2), "leases[1].HWAddr") + check(t, bytes.Equal(s.leases[1].IP, []byte{1, 1, 1, 2}), "leases[1].IP") + check(t, s.leases[1].Expiry.Unix() == 4000000002, "leases[1].Expiry") + + os.Remove("leases.db") +} From 6f69fb73af7901b52a4717833de8554069c0b43b Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 5 Mar 2019 18:04:49 +0300 Subject: [PATCH 06/14] * control: safely restart DHCP server * control: use mutex in all POST,PUT,DELETE handlers --- control.go | 3 +++ dhcp.go | 12 ++++++------ helpers.go | 6 ++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/control.go b/control.go index 0cd9d504..0bd7a5d7 100644 --- a/control.go +++ b/control.go @@ -12,6 +12,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/AdguardTeam/AdGuardHome/dnsforward" @@ -36,6 +37,8 @@ var client = &http.Client{ Timeout: time.Second * 30, } +var controlLock sync.Mutex + // ---------------- // helper functions // ---------------- diff --git a/dhcp.go b/dhcp.go index fe780305..58a3f161 100644 --- a/dhcp.go +++ b/dhcp.go @@ -50,6 +50,11 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { return } + err = dhcpServer.Stop() + if err != nil { + log.Error("failed to stop the DHCP server: %s", err) + } + if newconfig.Enabled { err := dhcpServer.Start(&newconfig) if err != nil { @@ -57,12 +62,7 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { return } } - if !newconfig.Enabled { - err := dhcpServer.Stop() - if err != nil { - log.Error("failed to stop the DHCP server: %s", err) - } - } + config.DHCP = newconfig httpUpdateConfigReloadDNSReturnOK(w, r) } diff --git a/helpers.go b/helpers.go index 25e35205..9d02ad3c 100644 --- a/helpers.go +++ b/helpers.go @@ -27,6 +27,12 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed) return } + + if method == "POST" || method == "PUT" || method == "DELETE" { + controlLock.Lock() + defer controlLock.Unlock() + } + handler(w, r) } } From b5f0d48e7f7b403b86b87d125ae48c518485dc82 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 5 Mar 2019 19:14:35 +0300 Subject: [PATCH 07/14] * DHCP: Stop(): wait until the worker is stopped --- dhcpd/dhcpd.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 3920bcef..d0c61934 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -40,6 +40,11 @@ type Server struct { ipnet *net.IPNet // if interface name changes, this needs to be reset + cond *sync.Cond // Synchronize worker thread with main thread + mutex sync.Mutex // Mutex for 'cond' + running bool // Set if the worker thread is running + stopping bool // Set if the worker thread should be stopped + // leases leases []*Lease leaseStart net.IP // parsed from config RangeStart @@ -131,14 +136,18 @@ func (s *Server) Start(config *ServerConfig) error { log.Info("DHCP: listening on 0.0.0.0:67") s.conn = c + s.cond = sync.NewCond(&s.mutex) + s.running = true go func() { // operate on c instead of c.conn because c.conn can change over time err := dhcp4.Serve(c, s) - if err != nil { + if err != nil && !s.stopping { log.Printf("dhcp4.Serve() returned with error: %s", err) } c.Close() // in case Serve() exits for other reason than listening socket closure + s.running = false + s.cond.Signal() }() return nil @@ -150,11 +159,22 @@ func (s *Server) Stop() error { // nothing to do, return silently return nil } + + s.stopping = true + err := s.closeConn() if err != nil { return wrapErrPrint(err, "Couldn't close UDP listening socket") } + // We've just closed the listening socket. + // Worker thread should exit right after it tries to read from the socket. + s.mutex.Lock() + for s.running { + s.cond.Wait() + } + s.mutex.Unlock() + s.dbStore() return nil } From 92cf7c1aca63afa3c16a6e6c327772646ac1781e Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 6 Mar 2019 15:13:27 +0300 Subject: [PATCH 08/14] * dhcp: refactor --- dhcpd/dhcpd.go | 144 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 50 deletions(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index d0c61934..f7d99d36 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net" + "strings" "sync" "time" @@ -59,6 +60,16 @@ type Server struct { sync.RWMutex } +// Print information about the available network interfaces +func printInterfaces() { + ifaces, _ := net.Interfaces() + var buf strings.Builder + for i := range ifaces { + buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name)) + } + log.Info("Available network interfaces: %s", buf.String()) +} + // Start will listen on port 67 and serve DHCP requests. // Even though config can be nil, it is not optional (at least for now), since there are no default values (yet). func (s *Server) Start(config *ServerConfig) error { @@ -69,6 +80,7 @@ func (s *Server) Start(config *ServerConfig) error { iface, err := net.InterfaceByName(s.InterfaceName) if err != nil { s.closeConn() // in case it was already started + printInterfaces() return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName) } @@ -189,6 +201,7 @@ func (s *Server) closeConn() error { return err } +// Reserve a lease for the client func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { // WARNING: do not remove copy() // the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call @@ -196,11 +209,6 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { hwaddrCOW := p.CHAddr() hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) copy(hwaddr, hwaddrCOW) - foundLease := s.locateLease(p) - if foundLease != nil { - // log.Tracef("found lease for %s: %+v", hwaddr, foundLease) - return foundLease, nil - } // not assigned a lease, create new one, find IP from LRU log.Tracef("Lease not found for %s: creating new one", hwaddr) ip, err := s.findFreeIP(hwaddr) @@ -216,7 +224,8 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { return lease, nil } -func (s *Server) locateLease(p dhcp4.Packet) *Lease { +// Find a lease for the client +func (s *Server) findLease(p dhcp4.Packet) *Lease { hwaddr := p.CHAddr() for i := range s.leases { if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) { @@ -243,7 +252,6 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { // if !bytes.Equal(foundHWaddr, hwaddr) { // log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr) // } - log.Tracef("will try again") continue } foundIP = newIP @@ -280,12 +288,7 @@ func (s *Server) unreserveIP(ip net.IP) { // ServeDHCP handles an incoming DHCP request func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Message from client %s: %d", p.CHAddr(), msgType) - - log.Tracef("Leases:") - for i, lease := range s.leases { - log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry) - } + s.printLeases() switch msgType { case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? @@ -297,26 +300,26 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh return s.handleDHCP4Request(p, options) case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP - log.Tracef("Got from client: Decline") + return s.handleDecline(p, options) case dhcp4.Release: // From Client, I don't need that IP anymore - log.Tracef("Got from client: Release") + return s.handleRelease(p, options) case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it - log.Tracef("Got from client: Inform") + return s.handleInform(p, options) // from server -- ignore those but enumerate just in case case dhcp4.Offer: // Broadcast From Server - Here's an IP - log.Printf("DHCP: received message from another server: Offer") + log.Printf("DHCP: received message from %s: Offer", p.CHAddr()) case dhcp4.ACK: // From Server, Yes you can have that IP - log.Printf("DHCP: received message from another server: ACK") + log.Printf("DHCP: received message from %s: ACK", p.CHAddr()) case dhcp4.NAK: // From Server, No you cannot have that IP - log.Printf("DHCP: received message from another server: NAK") + log.Printf("DHCP: received message from %s: NAK", p.CHAddr()) default: - log.Printf("DHCP: unknown packet %v from client %s", msgType, p.CHAddr()) + log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr()) return nil } return nil @@ -324,12 +327,23 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { // find a lease, but don't update lease time + var lease *Lease + var err error - lease, err := s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil + reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) + hostname := p.ParseOptions()[dhcp4.OptionHostName] + log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s", + reqIP, p.CHAddr(), hostname) + + lease = s.findLease(p) + for lease == nil { + lease, err = s.reserveLease(p) + if err != nil { + log.Error("Couldn't find free lease: %s", err) + return nil + } + + break } opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) @@ -339,49 +353,70 @@ func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pac } func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { + var lease *Lease + var err error + + reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) + log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s", + p.CIAddr(), reqIP, p.CHAddr()) + server := options[dhcp4.OptionServerIdentifier] if server != nil && !net.IP(server).Equal(s.ipnet.IP) { log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) return nil // Message not for this dhcp server } - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) if reqIP == nil { reqIP = p.CIAddr() - } - if reqIP.To4() == nil { - log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP) + } else if reqIP == nil || reqIP.To4() == nil { + log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) } - if reqIP.Equal(net.IPv4zero) { - log.Tracef("Replying with NAK: request IP is 0.0.0.0") + lease = s.findLease(p) + if lease == nil { + lease, err = s.reserveLease(p) + if err != nil { + log.Tracef("Couldn't find free lease: %s", err) + // couldn't find lease, don't respond + return nil + } + } + + if !lease.IP.Equal(reqIP) { + log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s", + lease.HWAddr, lease.IP, reqIP) return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) } - log.Tracef("requested IP is %s", reqIP) - lease, err := s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil - } + lease.Expiry = time.Now().Add(s.leaseTime) + log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s", + lease.IP, lease.HWAddr, lease.Expiry) + opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) + return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt) +} - if lease.IP.Equal(reqIP) { - // IP matches lease IP, nothing else to do - lease.Expiry = time.Now().Add(s.leaseTime) - log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr()) - opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt) - } +func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { + log.Tracef("Message from client: Inform. IP: %s HW: %s", + p.CIAddr(), p.CHAddr()) - // - // requested IP different from lease - // + return nil +} - log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) +func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { + log.Tracef("Message from client: Release. IP: %s HW: %s", + p.CIAddr(), p.CHAddr()) + + return nil +} + +func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { + reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) + log.Tracef("Message from client: Decline. IP: %s HW: %s", + reqIP, p.CHAddr()) + + return nil } // Leases returns the list of current DHCP leases (thread-safe) @@ -392,6 +427,15 @@ func (s *Server) Leases() []*Lease { return result } +// Print information about the current leases +func (s *Server) printLeases() { + log.Tracef("Leases:") + for i, lease := range s.leases { + log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", + i, lease.HWAddr, lease.IP, lease.Expiry) + } +} + // Reset internal state func (s *Server) reset() { s.Lock() From d832d7ce95478b595bbf966ffef644b6efaeee7d Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 7 Mar 2019 14:06:35 +0300 Subject: [PATCH 09/14] * dhcp: don't process Discover/Request packets with empty client HW address --- dhcpd/dhcpd.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index f7d99d36..86aa6fa9 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -325,6 +325,17 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh return nil } +// Return TRUE if DHCP packet is correct +func isValidPacket(p dhcp4.Packet) bool { + hw := p.CHAddr() + zeroes := make([]byte, len(hw)) + if bytes.Equal(hw, zeroes) { + log.Tracef("Packet has empty CHAddr") + return false + } + return true +} + func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { // find a lease, but don't update lease time var lease *Lease @@ -335,6 +346,10 @@ func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pac log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s", reqIP, p.CHAddr(), hostname) + if !isValidPacket(p) { + return nil + } + lease = s.findLease(p) for lease == nil { lease, err = s.reserveLease(p) @@ -360,6 +375,10 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s", p.CIAddr(), reqIP, p.CHAddr()) + if !isValidPacket(p) { + return nil + } + server := options[dhcp4.OptionServerIdentifier] if server != nil && !net.IP(server).Equal(s.ipnet.IP) { log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) From 542a67b84edc067d864164fbb9f566447a97ccb8 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 7 Mar 2019 16:57:20 +0300 Subject: [PATCH 10/14] * dhcp: don't allocate a new lease when processing Request message --- dhcpd/dhcpd.go | 9 ++------- dhcpd/dhcpd_test.go | 14 +++----------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 86aa6fa9..27233965 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -369,7 +369,6 @@ func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pac func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { var lease *Lease - var err error reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s", @@ -395,12 +394,8 @@ func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4 lease = s.findLease(p) if lease == nil { - lease, err = s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil - } + log.Tracef("Lease for %s isn't found", p.CHAddr()) + return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) } if !lease.IP.Equal(reqIP) { diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go index 2d675ed9..366606d4 100644 --- a/dhcpd/dhcpd_test.go +++ b/dhcpd/dhcpd_test.go @@ -43,11 +43,7 @@ func TestDHCP(t *testing.T) { lease, _ = s.reserveLease(p) check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") - - // Try to reserve another IP for the same machine - no new IP must be reserved - hw = []byte{1, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - lease, _ = s.reserveLease(p) + lease = s.findLease(p) check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") @@ -100,7 +96,7 @@ func misc(t *testing.T, s *Server) { p = make(dhcp4.Packet, 241) - // Commit a lease for an IP without prior Discover-Offer packets + // Try to commit a lease for an IP without prior Discover-Offer packets hw = []byte{2, 2, 3, 4, 5, 6} p.SetCHAddr(hw) p.SetCIAddr([]byte{0, 0, 0, 0}) @@ -108,11 +104,7 @@ func misc(t *testing.T, s *Server) { opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} p2 = s.handleDHCP4Request(p, opt) opt = p2.ParseOptions() - check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK") - check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr") - check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr") - check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") - check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") + check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK") } // Leases database store/load From 8fa2f481369677d650635ac981adba1c63ca58e6 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 7 Mar 2019 16:48:55 +0300 Subject: [PATCH 11/14] + dhcp: use ICMP for IP conflict detection + 'icmp_timeout_msec' YAML config setting --- dhcpd/dhcpd.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 3 files changed, 59 insertions(+) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 27233965..57e0c513 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -10,6 +10,7 @@ import ( "github.com/AdguardTeam/golibs/log" "github.com/krolaw/dhcp4" + "github.com/sparrc/go-ping" ) const defaultDiscoverTime = time.Second * 3 @@ -33,6 +34,10 @@ type ServerConfig struct { RangeStart string `json:"range_start" yaml:"range_start"` RangeEnd string `json:"range_end" yaml:"range_end"` LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds + + // IP conflict detector: time (ms) to wait for ICMP reply. + // 0: disable + ICMPTimeout uint `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"` } // Server - the current state of the DHCP server @@ -325,6 +330,51 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh return nil } +// Send ICMP to the specified machine +// Return TRUE if it doesn't reply, which probably means that the IP is available +func (s *Server) addrAvailable(target net.IP) bool { + + if s.ICMPTimeout == 0 { + return true + } + + pinger, err := ping.NewPinger(target.String()) + if err != nil { + log.Error("ping.NewPinger(): %v", err) + return true + } + + pinger.SetPrivileged(true) + pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond + pinger.Count = 1 + reply := false + pinger.OnRecv = func(pkt *ping.Packet) { + // log.Tracef("Received ICMP Reply from %v", target) + reply = true + } + log.Tracef("Sending ICMP Echo to %v", target) + pinger.Run() + + if reply { + log.Info("DHCP: IP conflict: %v is already used by another device", target) + return false + } + + log.Tracef("ICMP procedure is complete: %v", target) + return true +} + +// Add the specified IP to the black list for a time period +func (s *Server) blacklistLease(lease *Lease) { + hw := make(net.HardwareAddr, 6) + s.reserveIP(lease.IP, hw) + s.Lock() + lease.HWAddr = hw + lease.Hostname = "" + lease.Expiry = time.Now().Add(s.leaseTime) + s.Unlock() +} + // Return TRUE if DHCP packet is correct func isValidPacket(p dhcp4.Packet) bool { hw := p.CHAddr() @@ -358,6 +408,12 @@ func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pac return nil } + if !s.addrAvailable(lease.IP) { + s.blacklistLease(lease) + lease = nil + continue + } + break } diff --git a/go.mod b/go.mod index 2f0dd5bd..62035c9c 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/miekg/dns v1.1.1 github.com/shirou/gopsutil v2.18.10+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect + github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 github.com/stretchr/testify v1.2.2 go.uber.org/goleak v0.10.0 golang.org/x/net v0.0.0-20190119204137-ed066c81e75e diff --git a/go.sum b/go.sum index 51c15f76..c4bced69 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBh github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM= +github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= From d68600c5d03b73d94f380f5442c22ddc8f755d9a Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 11 Mar 2019 15:11:48 +0300 Subject: [PATCH 12/14] + dhcp: handle lease expiration --- dhcpd/dhcpd.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 57e0c513..b54195a8 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -215,14 +215,30 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) copy(hwaddr, hwaddrCOW) // not assigned a lease, create new one, find IP from LRU + hostname := p.ParseOptions()[dhcp4.OptionHostName] + lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)} + log.Tracef("Lease not found for %s: creating new one", hwaddr) ip, err := s.findFreeIP(hwaddr) if err != nil { - return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) + i := s.findExpiredLease() + if i < 0 { + return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) + } + + log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)", + s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry) + lease.IP = s.leases[i].IP + s.Lock() + s.leases[i] = lease + s.Unlock() + + s.reserveIP(lease.IP, hwaddr) + return lease, nil } + log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) - hostname := p.ParseOptions()[dhcp4.OptionHostName] - lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)} + lease.IP = ip s.Lock() s.leases = append(s.leases, lease) s.Unlock() @@ -241,6 +257,17 @@ func (s *Server) findLease(p dhcp4.Packet) *Lease { return nil } +// Find an expired lease and return its index or -1 +func (s *Server) findExpiredLease() int { + now := time.Now().Unix() + for i, lease := range s.leases { + if lease.Expiry.Unix() <= now { + return i + } + } + return -1 +} + func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { // if IP pool is nil, lazy initialize it if s.IPpool == nil { From 9494b87ca54a97baf2ccc78a8f9e593ad444bf81 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 11 Mar 2019 16:03:34 +0300 Subject: [PATCH 13/14] * control: add logs --- dhcp.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dhcp.go b/dhcp.go index 58a3f161..ac9c36c5 100644 --- a/dhcp.go +++ b/dhcp.go @@ -17,6 +17,7 @@ import ( var dhcpServer = dhcpd.Server{} func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) rawLeases := dhcpServer.Leases() leases := []map[string]string{} for i := range rawLeases { @@ -43,6 +44,7 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { } func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) newconfig := dhcpd.ServerConfig{} err := json.NewDecoder(r.Body).Decode(&newconfig) if err != nil { @@ -68,6 +70,7 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { } func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) response := map[string]interface{}{} ifaces, err := getValidNetInterfaces() @@ -128,6 +131,7 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) body, err := ioutil.ReadAll(r.Body) if err != nil { errorText := fmt.Sprintf("failed to read request body: %s", err) From ef637e1313ce548eab28c9b9f36c6db20a1c62c9 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 1 Mar 2019 17:39:20 +0300 Subject: [PATCH 14/14] + DHCP: step-by-step guide for test setup with Virtual Box --- dhcpd/README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 dhcpd/README.md diff --git a/dhcpd/README.md b/dhcpd/README.md new file mode 100644 index 00000000..83e48183 --- /dev/null +++ b/dhcpd/README.md @@ -0,0 +1,53 @@ +# DHCP server + +Contents: +* [Test setup with Virtual Box](#vbox) + + +## Test setup with Virtual Box + +To set up a test environment for DHCP server you need: + +* Linux host machine +* Virtual Box +* Virtual machine (guest OS doesn't matter) + +### Configure client + +1. Install Virtual Box and run the following command to create a Host-Only network: + + $ VBoxManage hostonlyif create + + You can check its status by `ip a` command. + + You can also set up Host-Only network using Virtual Box menu: + + File -> Host Network Manager... + +2. Create your virtual machine and set up its network: + + VM Settings -> Network -> Host-only Adapter + +3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server. + +### Configure server + +1. Edit server configuration file 'AdGuardHome.yaml', for example: + + dhcp: + enabled: true + interface_name: vboxnet0 + gateway_ip: 192.168.56.1 + subnet_mask: 255.255.255.0 + range_start: 192.168.56.2 + range_end: 192.168.56.2 + lease_duration: 86400 + icmp_timeout_msec: 1000 + +2. Start the server + + ./AdGuardHome + + There should be a message in log which shows that DHCP server is ready: + + [info] DHCP: listening on 0.0.0.0:67