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/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..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 {
@@ -50,6 +52,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,17 +64,13 @@ 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)
 }
 
 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)
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)
+
+<a id="vbox"></a>
+## 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
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 05d753b8..b54195a8 100644
--- a/dhcpd/dhcpd.go
+++ b/dhcpd/dhcpd.go
@@ -4,11 +4,13 @@ import (
 	"bytes"
 	"fmt"
 	"net"
+	"strings"
 	"sync"
 	"time"
 
 	"github.com/AdguardTeam/golibs/log"
 	"github.com/krolaw/dhcp4"
+	"github.com/sparrc/go-ping"
 )
 
 const defaultDiscoverTime = time.Second * 3
@@ -32,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
@@ -40,6 +46,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
@@ -54,6 +65,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 {
@@ -64,6 +85,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)
 	}
 
@@ -122,20 +144,27 @@ 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")
 	}
+	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
@@ -147,11 +176,23 @@ 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
 }
 
@@ -165,6 +206,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
@@ -172,27 +214,39 @@ 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
+	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()
 	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)) {
@@ -203,6 +257,17 @@ func (s *Server) locateLease(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 {
@@ -213,13 +278,12 @@ 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) {
 			// 	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
@@ -236,7 +300,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,133 +320,223 @@ 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("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)
-	}
+	s.printLeases()
 
 	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")
+		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")
-		// do nothing
+		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("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP 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("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP 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("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK")
+		log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
+
 	default:
-		log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType)
+		log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr())
 		return nil
 	}
 	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()
+	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
+	var err error
+
+	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)
+
+	if !isValidPacket(p) {
+		return nil
+	}
+
+	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
+		}
+
+		if !s.addrAvailable(lease.IP) {
+			s.blacklistLease(lease)
+			lease = nil
+			continue
+		}
+
+		break
+	}
+
+	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) {
+	var lease *Lease
+
+	reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
+	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)
 		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 {
+		log.Tracef("Lease for %s isn't found", p.CHAddr())
 		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
-	}
-
-	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())
-		return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
-	}
-
-	//
-	// requested IP different from lease
-	//
-
-	log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
-
-	hwaddr := s.getIPpool(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())
+	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)
 	}
 
-	// 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)
+	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)
 }
 
-// Leases returns the list of current DHCP leases
+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())
+
+	return 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)
 func (s *Server) Leases() []*Lease {
 	s.RLock()
 	result := s.leases
 	s.RUnlock()
 	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()
+	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..366606d4
--- /dev/null
+++ b/dhcpd/dhcpd_test.go
@@ -0,0 +1,152 @@
+package dhcpd
+
+import (
+	"bytes"
+	"net"
+	"os"
+	"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")
+	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")
+
+	// 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)
+
+	// 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})
+	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.NAK)}), "dhcp4.NAK")
+}
+
+// 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")
+}
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..62035c9c 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
@@ -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 6e4a79f1..c4bced69 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=
@@ -55,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=
diff --git a/helpers.go b/helpers.go
index 8e884d36..9d02ad3c 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
 // ----------------------------------
@@ -48,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)
 	}
 }
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