diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 138082c7..a4f8279c 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -467,6 +467,6 @@
     "static_ip_desc": "AdGuard Home is a server so it needs a static IP address to function properly. Otherwise, at some point, your router may assign a different IP address to this device.",
     "set_static_ip": "Set a static IP address",
     "install_static_ok": "Good news! The static IP address is already configured",
-    "install_static_error": "AdGuard Home cannot configure it automatically for your OS. Please look for an instruction on how to do this manually",
+    "install_static_error": "AdGuard Home cannot configure it automatically for this network interface. Please look for an instruction on how to do this manually.",
     "install_static_configure": "We have detected that a dynamic IP address is used — <0>{{ip}}</0>. Do you want to use it as your static address?"
 }
diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go
index 6031b9a8..9105d76b 100644
--- a/dhcpd/dhcp_http.go
+++ b/dhcpd/dhcp_http.go
@@ -10,6 +10,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/log"
 )
 
@@ -121,7 +123,7 @@ type netInterfaceJSON struct {
 func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
 	response := map[string]interface{}{}
 
-	ifaces, err := GetValidNetInterfaces()
+	ifaces, err := util.GetValidNetInterfaces()
 	if err != nil {
 		httpError(r, w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
 		return
@@ -219,7 +221,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
 		staticIP["error"] = err.Error()
 	} else if !isStaticIP {
 		staticIPStatus = "no"
-		staticIP["ip"] = GetFullIP(interfaceName)
+		staticIP["ip"] = util.GetSubnet(interfaceName)
 	}
 	staticIP["static"] = staticIPStatus
 
diff --git a/dhcpd/network_utils.go b/dhcpd/network_utils.go
index 204105ae..8407e031 100644
--- a/dhcpd/network_utils.go
+++ b/dhcpd/network_utils.go
@@ -6,37 +6,17 @@ import (
 	"io/ioutil"
 	"net"
 	"os/exec"
+	"regexp"
 	"runtime"
 	"strings"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/file"
 
 	"github.com/AdguardTeam/golibs/log"
 )
 
-// GetValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP
-// invalid interface is a ppp interface or the one that doesn't allow broadcasts
-func GetValidNetInterfaces() ([]net.Interface, error) {
-	ifaces, err := net.Interfaces()
-	if err != nil {
-		return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err)
-	}
-
-	netIfaces := []net.Interface{}
-
-	for i := range ifaces {
-		if ifaces[i].Flags&net.FlagPointToPoint != 0 {
-			// this interface is ppp, we're not interested in this one
-			continue
-		}
-
-		iface := ifaces[i]
-		netIfaces = append(netIfaces, iface)
-	}
-
-	return netIfaces, nil
-}
-
 // Check if network interface has a static IP configured
 // Supports: Raspbian.
 func HasStaticIP(ifaceName string) (bool, error) {
@@ -56,54 +36,18 @@ func HasStaticIP(ifaceName string) (bool, error) {
 	return false, fmt.Errorf("Cannot check if IP is static: not supported on %s", runtime.GOOS)
 }
 
-// Get IP address with netmask
-func GetFullIP(ifaceName string) string {
-	cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName)
-	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
-	d, err := cmd.Output()
-	if err != nil || cmd.ProcessState.ExitCode() != 0 {
-		return ""
-	}
-
-	fields := strings.Fields(string(d))
-	if len(fields) < 4 {
-		return ""
-	}
-	_, _, err = net.ParseCIDR(fields[3])
-	if err != nil {
-		return ""
-	}
-
-	return fields[3]
-}
-
-// Set a static IP for network interface
-// Supports: Raspbian.
+// Set a static IP for the specified network interface
 func SetStaticIP(ifaceName string) error {
-	ip := GetFullIP(ifaceName)
-	if len(ip) == 0 {
-		return errors.New("Can't get IP address")
+	if runtime.GOOS == "linux" {
+		return setStaticIPDhcpdConf(ifaceName)
 	}
 
-	ip4, _, err := net.ParseCIDR(ip)
-	if err != nil {
-		return err
-	}
-	gatewayIP := getGatewayIP(ifaceName)
-	add := setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String())
-
-	body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
-	if err != nil {
-		return err
+	if runtime.GOOS == "darwin" {
+		return fmt.Errorf("cannot do that")
+		// return setStaticIPDarwin(ifaceName)
 	}
 
-	body = append(body, []byte(add)...)
-	err = file.SafeWrite("/etc/dhcpcd.conf", body)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return fmt.Errorf("Cannot set static IP on %s", runtime.GOOS)
 }
 
 // for dhcpcd.conf
@@ -167,8 +111,37 @@ func getGatewayIP(ifaceName string) string {
 	return fields[2]
 }
 
+// setStaticIPDhcpdConf - updates /etc/dhcpd.conf and sets the current IP address to be static
+func setStaticIPDhcpdConf(ifaceName string) error {
+	ip := util.GetSubnet(ifaceName)
+	if len(ip) == 0 {
+		return errors.New("Can't get IP address")
+	}
+
+	ip4, _, err := net.ParseCIDR(ip)
+	if err != nil {
+		return err
+	}
+	gatewayIP := getGatewayIP(ifaceName)
+	add := updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String())
+
+	body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
+	if err != nil {
+		return err
+	}
+
+	body = append(body, []byte(add)...)
+	err = file.SafeWrite("/etc/dhcpcd.conf", body)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// updates dhcpd.conf content -- sets static IP address there
 // for dhcpcd.conf
-func setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
+func updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
 	var body []byte
 
 	add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
@@ -187,3 +160,154 @@ func setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
 
 	return string(body)
 }
+
+// Check if network interface has a static IP configured
+// Supports: MacOS.
+func hasStaticIPDarwin(ifaceName string) (bool, error) {
+	portInfo, err := getCurrentHardwarePortInfo(ifaceName)
+	if err != nil {
+		return false, err
+	}
+
+	return portInfo.static, nil
+}
+
+// setStaticIPDarwin - uses networksetup util to set the current IP address to be static
+// Additionally it configures the current DNS servers as well
+func setStaticIPDarwin(ifaceName string) error {
+	portInfo, err := getCurrentHardwarePortInfo(ifaceName)
+	if err != nil {
+		return err
+	}
+
+	if portInfo.static {
+		return errors.New("IP address is already static")
+	}
+
+	dnsAddrs, err := getEtcResolvConfServers()
+	if err != nil {
+		return err
+	}
+
+	args := make([]string, 0)
+	args = append(args, "-setdnsservers")
+	args = append(args, dnsAddrs...)
+
+	// Setting DNS servers is necessary when configuring a static IP
+	code, _, err := util.RunCommand("networksetup", args...)
+	if err != nil {
+		return err
+	}
+	if code != 0 {
+		return fmt.Errorf("Failed to set DNS servers, code=%d", code)
+	}
+
+	// Actually configures hardware port to have static IP
+	code, _, err = util.RunCommand("networksetup", "-setmanual",
+		portInfo.name, portInfo.ip, portInfo.subnet, portInfo.gatewayIP)
+	if err != nil {
+		return err
+	}
+	if code != 0 {
+		return fmt.Errorf("Failed to set DNS servers, code=%d", code)
+	}
+
+	return nil
+}
+
+// getCurrentHardwarePortInfo gets information the specified network interface
+func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) {
+	// First of all we should find hardware port name
+	m := getNetworkSetupHardwareReports()
+	hardwarePort, ok := m[ifaceName]
+	if !ok {
+		return hardwarePortInfo{}, fmt.Errorf("Could not find hardware port for %s", ifaceName)
+	}
+
+	return getHardwarePortInfo(hardwarePort)
+}
+
+// getNetworkSetupHardwareReports parses the output of the `networksetup -listallhardwareports` command
+// it returns a map where the key is the interface name, and the value is the "hardware port"
+// returns nil if it fails to parse the output
+func getNetworkSetupHardwareReports() map[string]string {
+	_, out, err := util.RunCommand("networksetup", "-listallhardwareports")
+	if err != nil {
+		return nil
+	}
+
+	re, err := regexp.Compile("Hardware Port: (.*?)\nDevice: (.*?)\n")
+	if err != nil {
+		return nil
+	}
+
+	m := make(map[string]string, 0)
+
+	matches := re.FindAllStringSubmatch(out, -1)
+	for i := range matches {
+		port := matches[i][1]
+		device := matches[i][2]
+		m[device] = port
+	}
+
+	return m
+}
+
+// hardwarePortInfo - information obtained using MacOS networksetup
+// about the current state of the internet connection
+type hardwarePortInfo struct {
+	name      string
+	ip        string
+	subnet    string
+	gatewayIP string
+	static    bool
+}
+
+func getHardwarePortInfo(hardwarePort string) (hardwarePortInfo, error) {
+	h := hardwarePortInfo{}
+
+	_, out, err := util.RunCommand("networksetup", "-getinfo", hardwarePort)
+	if err != nil {
+		return h, err
+	}
+
+	re := regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n")
+
+	match := re.FindStringSubmatch(out)
+	if len(match) == 0 {
+		return h, errors.New("Could not find hardware port info")
+	}
+
+	h.name = hardwarePort
+	h.ip = match[1]
+	h.subnet = match[2]
+	h.gatewayIP = match[3]
+
+	if strings.Index(out, "Manual Configuration") == 0 {
+		h.static = true
+	}
+
+	return h, nil
+}
+
+// Gets a list of nameservers currently configured in the /etc/resolv.conf
+func getEtcResolvConfServers() ([]string, error) {
+	body, err := ioutil.ReadFile("/etc/resolv.conf")
+	if err != nil {
+		return nil, err
+	}
+
+	re := regexp.MustCompile("nameserver ([a-zA-Z0-9.:]+)")
+
+	matches := re.FindAllStringSubmatch(string(body), -1)
+	if len(matches) == 0 {
+		return nil, errors.New("Found no DNS servers in /etc/resolv.conf")
+	}
+
+	addrs := make([]string, 0)
+	for i := range matches {
+		addrs = append(addrs, matches[i][1])
+	}
+
+	return addrs, nil
+}
diff --git a/dhcpd/network_utils_darwin.go b/dhcpd/network_utils_darwin.go
deleted file mode 100644
index 9110b0b0..00000000
--- a/dhcpd/network_utils_darwin.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package dhcpd
-
-// Check if network interface has a static IP configured
-// Supports: Raspbian.
-func hasStaticIPDarwin(ifaceName string) (bool, error) {
-
-	return false, nil
-}
diff --git a/dhcpd/network_utils_test.go b/dhcpd/network_utils_test.go
index 3948a224..2957a411 100644
--- a/dhcpd/network_utils_test.go
+++ b/dhcpd/network_utils_test.go
@@ -46,7 +46,7 @@ static routers=192.168.0.1
 static domain_name_servers=192.168.0.2
 
 `
-	s := setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2")
+	s := updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2")
 	assert.Equal(t, dhcpcdConf, s)
 
 	// without gateway
@@ -56,6 +56,6 @@ static ip_address=192.168.0.2/24
 static domain_name_servers=192.168.0.2
 
 `
-	s = setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2")
+	s = updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2")
 	assert.Equal(t, dhcpcdConf, s)
 }
diff --git a/home/control.go b/home/control.go
index d852ef66..031f7a36 100644
--- a/home/control.go
+++ b/home/control.go
@@ -3,7 +3,13 @@ package home
 import (
 	"encoding/json"
 	"fmt"
+	"net"
 	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/AdguardTeam/AdGuardHome/util"
 
 	"github.com/AdguardTeam/AdGuardHome/dnsforward"
 	"github.com/AdguardTeam/golibs/log"
@@ -54,8 +60,7 @@ func getDNSAddresses() []string {
 	dnsAddresses := []string{}
 
 	if config.DNS.BindHost == "0.0.0.0" {
-
-		ifaces, e := getValidNetInterfacesForWeb()
+		ifaces, e := util.GetValidNetInterfacesForWeb()
 		if e != nil {
 			log.Error("Couldn't get network interfaces: %v", e)
 			return []string{}
@@ -66,7 +71,6 @@ func getDNSAddresses() []string {
 				addDNSAddress(&dnsAddresses, addr)
 			}
 		}
-
 	} else {
 		addDNSAddress(&dnsAddresses, config.DNS.BindHost)
 	}
@@ -180,3 +184,118 @@ func registerControlHandlers() {
 func httpRegister(method string, url string, handler func(http.ResponseWriter, *http.Request)) {
 	http.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
 }
+
+// ----------------------------------
+// helper functions for HTTP handlers
+// ----------------------------------
+func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		log.Debug("%s %v", r.Method, r.URL)
+
+		if r.Method != method {
+			http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
+			return
+		}
+
+		if method == "POST" || method == "PUT" || method == "DELETE" {
+			Context.controlLock.Lock()
+			defer Context.controlLock.Unlock()
+		}
+
+		handler(w, r)
+	}
+}
+
+func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return ensure("POST", handler)
+}
+
+func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return ensure("GET", handler)
+}
+
+// Bridge between http.Handler object and Go function
+type httpHandler struct {
+	handler func(http.ResponseWriter, *http.Request)
+}
+
+func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	h.handler(w, r)
+}
+
+func ensureHandler(method string, handler func(http.ResponseWriter, *http.Request)) http.Handler {
+	h := httpHandler{}
+	h.handler = ensure(method, handler)
+	return &h
+}
+
+// preInstall lets the handler run only if firstRun is true, no redirects
+func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if !Context.firstRun {
+			// if it's not first run, don't let users access it (for example /install.html when configuration is done)
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
+		handler(w, r)
+	}
+}
+
+// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
+type preInstallHandlerStruct struct {
+	handler http.Handler
+}
+
+func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	preInstall(p.handler.ServeHTTP)(w, r)
+}
+
+// preInstallHandler returns http.Handler interface for preInstall wrapper
+func preInstallHandler(handler http.Handler) http.Handler {
+	return &preInstallHandlerStruct{handler}
+}
+
+// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
+// it also enforces HTTPS if it is enabled and configured
+func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if Context.firstRun &&
+			!strings.HasPrefix(r.URL.Path, "/install.") &&
+			r.URL.Path != "/favicon.png" {
+			http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
+			return
+		}
+		// enforce https?
+		if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && Context.httpsServer.server != nil {
+			// yes, and we want host from host:port
+			host, _, err := net.SplitHostPort(r.Host)
+			if err != nil {
+				// no port in host
+				host = r.Host
+			}
+			// construct new URL to redirect to
+			newURL := url.URL{
+				Scheme:   "https",
+				Host:     net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
+				Path:     r.URL.Path,
+				RawQuery: r.URL.RawQuery,
+			}
+			http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
+			return
+		}
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		handler(w, r)
+	}
+}
+
+type postInstallHandlerStruct struct {
+	handler http.Handler
+}
+
+func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	postInstall(p.handler.ServeHTTP)(w, r)
+}
+
+func postInstallHandler(handler http.Handler) http.Handler {
+	return &postInstallHandlerStruct{handler}
+}
diff --git a/home/control_install.go b/home/control_install.go
index 8a595bb8..50d6e7c1 100644
--- a/home/control_install.go
+++ b/home/control_install.go
@@ -13,6 +13,8 @@ import (
 	"runtime"
 	"strconv"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/AdGuardHome/dhcpd"
 
 	"github.com/AdguardTeam/golibs/log"
@@ -38,7 +40,7 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
 	data.WebPort = 80
 	data.DNSPort = 53
 
-	ifaces, err := getValidNetInterfacesForWeb()
+	ifaces, err := util.GetValidNetInterfacesForWeb()
 	if err != nil {
 		httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
 		return
@@ -101,16 +103,16 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
-		err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port)
+		err = util.CheckPortAvailable(reqData.Web.IP, reqData.Web.Port)
 		if err != nil {
 			respData.Web.Status = fmt.Sprintf("%v", err)
 		}
 	}
 
 	if reqData.DNS.Port != 0 {
-		err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
+		err = util.CheckPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
 
-		if errorIsAddrInUse(err) {
+		if util.ErrorIsAddrInUse(err) {
 			canAutofix := checkDNSStubListener()
 			if canAutofix && reqData.DNS.Autofix {
 
@@ -119,7 +121,7 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
 					log.Error("Couldn't disable DNSStubListener: %s", err)
 				}
 
-				err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
+				err = util.CheckPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
 				canAutofix = false
 			}
 
@@ -127,26 +129,22 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if err == nil {
-			err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
+			err = util.CheckPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
 		}
 
 		if err != nil {
 			respData.DNS.Status = fmt.Sprintf("%v", err)
-
 		} else {
-
-			interfaceName := getInterfaceByIP(reqData.DNS.IP)
+			interfaceName := util.GetInterfaceByIP(reqData.DNS.IP)
 			staticIPStatus := "yes"
 
 			if len(interfaceName) == 0 {
 				staticIPStatus = "error"
 				respData.StaticIP.Error = fmt.Sprintf("Couldn't find network interface by IP %s", reqData.DNS.IP)
-
 			} else if reqData.DNS.SetStaticIP {
 				err = dhcpd.SetStaticIP(interfaceName)
 				staticIPStatus = "error"
 				respData.StaticIP.Error = err.Error()
-
 			} else {
 				// check if we have a static IP
 				isStaticIP, err := dhcpd.HasStaticIP(interfaceName)
@@ -155,7 +153,7 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
 					respData.StaticIP.Error = err.Error()
 				} else if !isStaticIP {
 					staticIPStatus = "no"
-					respData.StaticIP.IP = dhcpd.GetFullIP(interfaceName)
+					respData.StaticIP.IP = util.GetSubnet(interfaceName)
 				}
 			}
 			respData.StaticIP.Static = staticIPStatus
@@ -279,7 +277,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 
 	// validate that hosts and ports are bindable
 	if restartHTTP {
-		err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
+		err = util.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
 		if err != nil {
 			httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s",
 				net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
@@ -287,13 +285,13 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
+	err = util.CheckPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
 	if err != nil {
 		httpError(w, http.StatusBadRequest, "%s", err)
 		return
 	}
 
-	err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
+	err = util.CheckPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
 	if err != nil {
 		httpError(w, http.StatusBadRequest, "%s", err)
 		return
diff --git a/home/control_tls.go b/home/control_tls.go
index f0f4c655..0df8b729 100644
--- a/home/control_tls.go
+++ b/home/control_tls.go
@@ -20,6 +20,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/log"
 	"github.com/joomcode/errorx"
 )
@@ -84,7 +86,7 @@ func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
 		alreadyRunning = true
 	}
 	if !alreadyRunning {
-		err = checkPortAvailable(config.BindHost, data.PortHTTPS)
+		err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
 		if err != nil {
 			httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
 			return
@@ -114,7 +116,7 @@ func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
 		alreadyRunning = true
 	}
 	if !alreadyRunning {
-		err = checkPortAvailable(config.BindHost, data.PortHTTPS)
+		err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
 		if err != nil {
 			httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
 			return
diff --git a/home/control_update.go b/home/control_update.go
index 10e94e50..87fe4034 100644
--- a/home/control_update.go
+++ b/home/control_update.go
@@ -17,6 +17,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/log"
 )
 
@@ -196,7 +198,7 @@ func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
 		binName = "AdGuardHome.exe"
 	}
 	u.curBinName = filepath.Join(workDir, binName)
-	if !fileExists(u.curBinName) {
+	if !util.FileExists(u.curBinName) {
 		return nil, fmt.Errorf("Executable file %s doesn't exist", u.curBinName)
 	}
 	u.bkpBinName = filepath.Join(u.backupDir, binName)
diff --git a/home/control_update_test.go b/home/control_update_test.go
index cfc13823..6ec4a186 100644
--- a/home/control_update_test.go
+++ b/home/control_update_test.go
@@ -8,7 +8,6 @@ import (
 )
 
 func TestDoUpdate(t *testing.T) {
-
 	config.DNS.Port = 0
 	Context.workDir = "..." // set absolute path
 	newver := "v0.96"
diff --git a/home/filter.go b/home/filter.go
index 9e0a5abf..befdd873 100644
--- a/home/filter.go
+++ b/home/filter.go
@@ -13,6 +13,7 @@ import (
 	"time"
 
 	"github.com/AdguardTeam/AdGuardHome/dnsfilter"
+	"github.com/AdguardTeam/AdGuardHome/util"
 	"github.com/AdguardTeam/golibs/file"
 	"github.com/AdguardTeam/golibs/log"
 )
@@ -401,7 +402,7 @@ func parseFilterContents(contents []byte) (int, string) {
 
 	// Count lines in the filter
 	for len(data) != 0 {
-		line := SplitNext(&data, '\n')
+		line := util.SplitNext(&data, '\n')
 		if len(line) == 0 {
 			continue
 		}
diff --git a/home/filter_test.go b/home/filter_test.go
index fe1826d2..edda556a 100644
--- a/home/filter_test.go
+++ b/home/filter_test.go
@@ -10,6 +10,11 @@ import (
 )
 
 func TestFilters(t *testing.T) {
+	dir := prepareTestDir()
+	defer func() { _ = os.RemoveAll(dir) }()
+
+	Context = homeContext{}
+	Context.workDir = dir
 	Context.client = &http.Client{
 		Timeout: time.Minute * 5,
 	}
@@ -33,5 +38,5 @@ func TestFilters(t *testing.T) {
 	assert.True(t, err == nil)
 
 	f.unload()
-	os.Remove(f.Path())
+	_ = os.Remove(f.Path())
 }
diff --git a/home/helpers.go b/home/helpers.go
deleted file mode 100644
index 5a87aec7..00000000
--- a/home/helpers.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package home
-
-import (
-	"context"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-	"runtime"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/AdguardTeam/golibs/log"
-	"github.com/joomcode/errorx"
-)
-
-// ----------------------------------
-// helper functions for HTTP handlers
-// ----------------------------------
-func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
-	return func(w http.ResponseWriter, r *http.Request) {
-		log.Debug("%s %v", r.Method, r.URL)
-
-		if r.Method != method {
-			http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
-			return
-		}
-
-		if method == "POST" || method == "PUT" || method == "DELETE" {
-			Context.controlLock.Lock()
-			defer Context.controlLock.Unlock()
-		}
-
-		handler(w, r)
-	}
-}
-
-func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
-	return ensure("POST", handler)
-}
-
-func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
-	return ensure("GET", handler)
-}
-
-// Bridge between http.Handler object and Go function
-type httpHandler struct {
-	handler func(http.ResponseWriter, *http.Request)
-}
-
-func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	h.handler(w, r)
-}
-
-func ensureHandler(method string, handler func(http.ResponseWriter, *http.Request)) http.Handler {
-	h := httpHandler{}
-	h.handler = ensure(method, handler)
-	return &h
-}
-
-// -------------------
-// first run / install
-// -------------------
-func detectFirstRun() bool {
-	configfile := Context.configFilename
-	if !filepath.IsAbs(configfile) {
-		configfile = filepath.Join(Context.workDir, Context.configFilename)
-	}
-	_, err := os.Stat(configfile)
-	if !os.IsNotExist(err) {
-		// do nothing, file exists
-		return false
-	}
-	return true
-}
-
-// preInstall lets the handler run only if firstRun is true, no redirects
-func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
-	return func(w http.ResponseWriter, r *http.Request) {
-		if !Context.firstRun {
-			// if it's not first run, don't let users access it (for example /install.html when configuration is done)
-			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
-			return
-		}
-		handler(w, r)
-	}
-}
-
-// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
-type preInstallHandlerStruct struct {
-	handler http.Handler
-}
-
-func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	preInstall(p.handler.ServeHTTP)(w, r)
-}
-
-// preInstallHandler returns http.Handler interface for preInstall wrapper
-func preInstallHandler(handler http.Handler) http.Handler {
-	return &preInstallHandlerStruct{handler}
-}
-
-// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
-// it also enforces HTTPS if it is enabled and configured
-func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
-	return func(w http.ResponseWriter, r *http.Request) {
-		if Context.firstRun &&
-			!strings.HasPrefix(r.URL.Path, "/install.") &&
-			r.URL.Path != "/favicon.png" {
-			http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
-			return
-		}
-		// enforce https?
-		if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && Context.httpsServer.server != nil {
-			// yes, and we want host from host:port
-			host, _, err := net.SplitHostPort(r.Host)
-			if err != nil {
-				// no port in host
-				host = r.Host
-			}
-			// construct new URL to redirect to
-			newURL := url.URL{
-				Scheme:   "https",
-				Host:     net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
-				Path:     r.URL.Path,
-				RawQuery: r.URL.RawQuery,
-			}
-			http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
-			return
-		}
-		w.Header().Set("Access-Control-Allow-Origin", "*")
-		handler(w, r)
-	}
-}
-
-type postInstallHandlerStruct struct {
-	handler http.Handler
-}
-
-func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	postInstall(p.handler.ServeHTTP)(w, r)
-}
-
-func postInstallHandler(handler http.Handler) http.Handler {
-	return &postInstallHandlerStruct{handler}
-}
-
-// Connect to a remote server resolving hostname using our own DNS server
-func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
-	log.Tracef("network:%v  addr:%v", network, addr)
-
-	host, port, err := net.SplitHostPort(addr)
-	if err != nil {
-		return nil, err
-	}
-
-	dialer := &net.Dialer{
-		Timeout: time.Minute * 5,
-	}
-
-	if net.ParseIP(host) != nil || config.DNS.Port == 0 {
-		con, err := dialer.DialContext(ctx, network, addr)
-		return con, err
-	}
-
-	addrs, e := Context.dnsServer.Resolve(host)
-	log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
-	if e != nil {
-		return nil, e
-	}
-
-	if len(addrs) == 0 {
-		return nil, fmt.Errorf("couldn't lookup host: %s", host)
-	}
-
-	var dialErrs []error
-	for _, a := range addrs {
-		addr = net.JoinHostPort(a.String(), port)
-		con, err := dialer.DialContext(ctx, network, addr)
-		if err != nil {
-			dialErrs = append(dialErrs, err)
-			continue
-		}
-		return con, err
-	}
-	return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
-}
-
-// ---------------------
-// general helpers
-// ---------------------
-
-// fileExists returns TRUE if file exists
-func fileExists(fn string) bool {
-	_, err := os.Stat(fn)
-	if err != nil {
-		return false
-	}
-	return true
-}
-
-// runCommand runs shell command
-func runCommand(command string, arguments ...string) (int, string, error) {
-	cmd := exec.Command(command, arguments...)
-	out, err := cmd.Output()
-	if err != nil {
-		return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
-	}
-
-	return cmd.ProcessState.ExitCode(), string(out), nil
-}
-
-// ---------------------
-// debug logging helpers
-// ---------------------
-func _Func() string {
-	pc := make([]uintptr, 10) // at least 1 entry needed
-	runtime.Callers(2, pc)
-	f := runtime.FuncForPC(pc[0])
-	return path.Base(f.Name())
-}
-
-// SplitNext - split string by a byte and return the first chunk
-// Whitespace is trimmed
-func SplitNext(str *string, splitBy byte) string {
-	i := strings.IndexByte(*str, splitBy)
-	s := ""
-	if i != -1 {
-		s = (*str)[0:i]
-		*str = (*str)[i+1:]
-	} else {
-		s = *str
-		*str = ""
-	}
-	return strings.TrimSpace(s)
-}
diff --git a/home/home.go b/home/home.go
index b31f1044..f7c697f6 100644
--- a/home/home.go
+++ b/home/home.go
@@ -20,6 +20,10 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
+	"github.com/joomcode/errorx"
+
 	"github.com/AdguardTeam/AdGuardHome/isdelve"
 
 	"github.com/AdguardTeam/AdGuardHome/dhcpd"
@@ -193,7 +197,7 @@ func run(args options) {
 
 	if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
 		config.RlimitNoFile != 0 {
-		setRlimit(config.RlimitNoFile)
+		util.SetRlimit(config.RlimitNoFile)
 	}
 
 	// override bind host/port from the console
@@ -327,7 +331,7 @@ func httpServerLoop() {
 // Check if the current user has root (administrator) rights
 //  and if not, ask and try to run as root
 func requireAdminRights() {
-	admin, _ := haveAdminRights()
+	admin, _ := util.HaveAdminRights()
 	if //noinspection ALL
 	admin || isdelve.Enabled {
 		return
@@ -412,7 +416,7 @@ func configureLogger(args options) {
 
 	if ls.LogFile == configSyslog {
 		// Use syslog where it is possible and eventlog on Windows
-		err := configureSyslog()
+		err := util.ConfigureSyslog(serviceName)
 		if err != nil {
 			log.Fatalf("cannot initialize syslog: %s", err)
 		}
@@ -448,9 +452,9 @@ func stopHTTPServer() {
 	log.Info("Stopping HTTP server...")
 	Context.httpsServer.shutdown = true
 	if Context.httpsServer.server != nil {
-		Context.httpsServer.server.Shutdown(context.TODO())
+		_ = Context.httpsServer.server.Shutdown(context.TODO())
 	}
-	Context.httpServer.Shutdown(context.TODO())
+	_ = Context.httpServer.Shutdown(context.TODO())
 	log.Info("Stopped HTTP server")
 }
 
@@ -580,7 +584,7 @@ func printHTTPAddresses(proto string) {
 		}
 	} else if config.BindHost == "0.0.0.0" {
 		log.Println("AdGuard Home is available on the following addresses:")
-		ifaces, err := getValidNetInterfacesForWeb()
+		ifaces, err := util.GetValidNetInterfacesForWeb()
 		if err != nil {
 			// That's weird, but we'll ignore it
 			address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
@@ -597,3 +601,60 @@ func printHTTPAddresses(proto string) {
 		log.Printf("Go to %s://%s", proto, address)
 	}
 }
+
+// -------------------
+// first run / install
+// -------------------
+func detectFirstRun() bool {
+	configfile := Context.configFilename
+	if !filepath.IsAbs(configfile) {
+		configfile = filepath.Join(Context.workDir, Context.configFilename)
+	}
+	_, err := os.Stat(configfile)
+	if !os.IsNotExist(err) {
+		// do nothing, file exists
+		return false
+	}
+	return true
+}
+
+// Connect to a remote server resolving hostname using our own DNS server
+func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
+	log.Tracef("network:%v  addr:%v", network, addr)
+
+	host, port, err := net.SplitHostPort(addr)
+	if err != nil {
+		return nil, err
+	}
+
+	dialer := &net.Dialer{
+		Timeout: time.Minute * 5,
+	}
+
+	if net.ParseIP(host) != nil || config.DNS.Port == 0 {
+		con, err := dialer.DialContext(ctx, network, addr)
+		return con, err
+	}
+
+	addrs, e := Context.dnsServer.Resolve(host)
+	log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
+	if e != nil {
+		return nil, e
+	}
+
+	if len(addrs) == 0 {
+		return nil, fmt.Errorf("couldn't lookup host: %s", host)
+	}
+
+	var dialErrs []error
+	for _, a := range addrs {
+		addr = net.JoinHostPort(a.String(), port)
+		con, err := dialer.DialContext(ctx, network, addr)
+		if err != nil {
+			dialErrs = append(dialErrs, err)
+			continue
+		}
+		return con, err
+	}
+	return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
+}
diff --git a/home/service.go b/home/service.go
index 1951ed28..d066e118 100644
--- a/home/service.go
+++ b/home/service.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"syscall"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
 	"github.com/AdguardTeam/golibs/log"
 	"github.com/kardianos/service"
 )
@@ -229,7 +230,7 @@ func configureService(c *service.Config) {
 // returns command code or error if any
 func runInitdCommand(action string) (int, error) {
 	confPath := "/etc/init.d/" + serviceName
-	code, _, err := runCommand("sh", "-c", confPath+" "+action)
+	code, _, err := util.RunCommand("sh", "-c", confPath+" "+action)
 	return code, err
 }
 
diff --git a/home/upgrade.go b/home/upgrade.go
index 7336f846..b2936a3e 100644
--- a/home/upgrade.go
+++ b/home/upgrade.go
@@ -5,6 +5,8 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/file"
 	"github.com/AdguardTeam/golibs/log"
 	"golang.org/x/crypto/bcrypt"
@@ -114,7 +116,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
 // The first schema upgrade:
 // No more "dnsfilter.txt", filters are now kept in data/filters/
 func upgradeSchema0to1(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	dnsFilterPath := filepath.Join(Context.workDir, "dnsfilter.txt")
 	if _, err := os.Stat(dnsFilterPath); !os.IsNotExist(err) {
@@ -135,7 +137,7 @@ func upgradeSchema0to1(diskConfig *map[string]interface{}) error {
 // coredns is now dns in config
 // delete 'Corefile', since we don't use that anymore
 func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	coreFilePath := filepath.Join(Context.workDir, "Corefile")
 	if _, err := os.Stat(coreFilePath); !os.IsNotExist(err) {
@@ -159,7 +161,7 @@ func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
 // Third schema upgrade:
 // Bootstrap DNS becomes an array
 func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	// Let's read dns configuration from diskConfig
 	dnsConfig, ok := (*diskConfig)["dns"]
@@ -196,7 +198,7 @@ func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
 
 // Add use_global_blocked_services=true setting for existing "clients" array
 func upgradeSchema3to4(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	(*diskConfig)["schema_version"] = 4
 
@@ -233,7 +235,7 @@ func upgradeSchema3to4(diskConfig *map[string]interface{}) error {
 //   password: "..."
 // ...
 func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	(*diskConfig)["schema_version"] = 5
 
@@ -288,7 +290,7 @@ func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
 //   - 127.0.0.1
 //   - ...
 func upgradeSchema5to6(diskConfig *map[string]interface{}) error {
-	log.Printf("%s(): called", _Func())
+	log.Printf("%s(): called", util.FuncName())
 
 	(*diskConfig)["schema_version"] = 6
 
diff --git a/home/whois.go b/home/whois.go
index 25fe211a..321b4ef2 100644
--- a/home/whois.go
+++ b/home/whois.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/cache"
 	"github.com/AdguardTeam/golibs/log"
 )
@@ -61,7 +63,7 @@ func whoisParse(data string) map[string]string {
 	descr := ""
 	netname := ""
 	for len(data) != 0 {
-		ln := SplitNext(&data, '\n')
+		ln := util.SplitNext(&data, '\n')
 		if len(ln) == 0 || ln[0] == '#' || ln[0] == '%' {
 			continue
 		}
diff --git a/util/helpers.go b/util/helpers.go
new file mode 100644
index 00000000..730e0a2c
--- /dev/null
+++ b/util/helpers.go
@@ -0,0 +1,59 @@
+package util
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path"
+	"runtime"
+	"strings"
+)
+
+// ---------------------
+// general helpers
+// ---------------------
+
+// fileExists returns TRUE if file exists
+func FileExists(fn string) bool {
+	_, err := os.Stat(fn)
+	if err != nil {
+		return false
+	}
+	return true
+}
+
+// runCommand runs shell command
+func RunCommand(command string, arguments ...string) (int, string, error) {
+	cmd := exec.Command(command, arguments...)
+	out, err := cmd.Output()
+	if err != nil {
+		return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
+	}
+
+	return cmd.ProcessState.ExitCode(), string(out), nil
+}
+
+// ---------------------
+// debug logging helpers
+// ---------------------
+func FuncName() string {
+	pc := make([]uintptr, 10) // at least 1 entry needed
+	runtime.Callers(2, pc)
+	f := runtime.FuncForPC(pc[0])
+	return path.Base(f.Name())
+}
+
+// SplitNext - split string by a byte and return the first chunk
+// Whitespace is trimmed
+func SplitNext(str *string, splitBy byte) string {
+	i := strings.IndexByte(*str, splitBy)
+	s := ""
+	if i != -1 {
+		s = (*str)[0:i]
+		*str = (*str)[i+1:]
+	} else {
+		s = *str
+		*str = ""
+	}
+	return strings.TrimSpace(s)
+}
diff --git a/util/helpers_test.go b/util/helpers_test.go
new file mode 100644
index 00000000..d5e90637
--- /dev/null
+++ b/util/helpers_test.go
@@ -0,0 +1,14 @@
+package util
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSplitNext(t *testing.T) {
+	s := " a,b , c "
+	assert.True(t, SplitNext(&s, ',') == "a")
+	assert.True(t, SplitNext(&s, ',') == "b")
+	assert.True(t, SplitNext(&s, ',') == "c" && len(s) == 0)
+}
diff --git a/home/network_utils.go b/util/network_utils.go
similarity index 52%
rename from home/network_utils.go
rename to util/network_utils.go
index 4277c1c2..af410201 100644
--- a/home/network_utils.go
+++ b/util/network_utils.go
@@ -1,4 +1,4 @@
-package home
+package util
 
 import (
 	"errors"
@@ -10,23 +10,48 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/AdguardTeam/AdGuardHome/dhcpd"
+	"github.com/AdguardTeam/golibs/log"
 
 	"github.com/joomcode/errorx"
 )
 
-type netInterface struct {
-	Name         string
-	MTU          int
-	HardwareAddr string
-	Addresses    []string
-	Flags        string
+// NetInterface represents a list of network interfaces
+type NetInterface struct {
+	Name         string   // Network interface name
+	MTU          int      // MTU
+	HardwareAddr string   // Hardware address
+	Addresses    []string // Array with the network interface addresses
+	Subnets      []string // Array with CIDR addresses of this network interface
+	Flags        string   // Network interface flags (up, broadcast, etc)
+}
+
+// GetValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP
+// invalid interface is a ppp interface or the one that doesn't allow broadcasts
+func GetValidNetInterfaces() ([]net.Interface, error) {
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err)
+	}
+
+	netIfaces := []net.Interface{}
+
+	for i := range ifaces {
+		if ifaces[i].Flags&net.FlagPointToPoint != 0 {
+			// this interface is ppp, we're not interested in this one
+			continue
+		}
+
+		iface := ifaces[i]
+		netIfaces = append(netIfaces, iface)
+	}
+
+	return netIfaces, nil
 }
 
 // getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only
 // we do not return link-local addresses here
-func getValidNetInterfacesForWeb() ([]netInterface, error) {
-	ifaces, err := dhcpd.GetValidNetInterfaces()
+func GetValidNetInterfacesForWeb() ([]NetInterface, error) {
+	ifaces, err := GetValidNetInterfaces()
 	if err != nil {
 		return nil, errorx.Decorate(err, "Couldn't get interfaces")
 	}
@@ -34,7 +59,7 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
 		return nil, errors.New("couldn't find any legible interface")
 	}
 
-	var netInterfaces []netInterface
+	var netInterfaces []NetInterface
 
 	for _, iface := range ifaces {
 		addrs, e := iface.Addrs()
@@ -42,7 +67,7 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
 			return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
 		}
 
-		netIface := netInterface{
+		netIface := NetInterface{
 			Name:         iface.Name,
 			MTU:          iface.MTU,
 			HardwareAddr: iface.HardwareAddr.String(),
@@ -52,19 +77,26 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
 			netIface.Flags = iface.Flags.String()
 		}
 
-		// we don't want link-local addresses in json, so skip them
+		// Collect network interface addresses
 		for _, addr := range addrs {
-			ipnet, ok := addr.(*net.IPNet)
+			ipNet, ok := addr.(*net.IPNet)
 			if !ok {
 				// not an IPNet, should not happen
 				return nil, fmt.Errorf("got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
 			}
 			// ignore link-local
-			if ipnet.IP.IsLinkLocalUnicast() {
+			if ipNet.IP.IsLinkLocalUnicast() {
 				continue
 			}
-			netIface.Addresses = append(netIface.Addresses, ipnet.IP.String())
+			// ignore IPv6
+			if ipNet.IP.To4() == nil {
+				continue
+			}
+			netIface.Addresses = append(netIface.Addresses, ipNet.IP.String())
+			netIface.Subnets = append(netIface.Subnets, ipNet.String())
 		}
+
+		// Discard interfaces with no addresses
 		if len(netIface.Addresses) != 0 {
 			netInterfaces = append(netInterfaces, netIface)
 		}
@@ -74,8 +106,8 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
 }
 
 // Get interface name by its IP address.
-func getInterfaceByIP(ip string) string {
-	ifaces, err := getValidNetInterfacesForWeb()
+func GetInterfaceByIP(ip string) string {
+	ifaces, err := GetValidNetInterfacesForWeb()
 	if err != nil {
 		return ""
 	}
@@ -91,8 +123,26 @@ func getInterfaceByIP(ip string) string {
 	return ""
 }
 
+// Get IP address with netmask for the specified interface
+// Returns an empty string if it fails to find it
+func GetSubnet(ifaceName string) string {
+	netIfaces, err := GetValidNetInterfacesForWeb()
+	if err != nil {
+		log.Error("Could not get network interfaces info: %v", err)
+		return ""
+	}
+
+	for _, netIface := range netIfaces {
+		if netIface.Name == ifaceName && len(netIface.Subnets) > 0 {
+			return netIface.Subnets[0]
+		}
+	}
+
+	return ""
+}
+
 // checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily
-func checkPortAvailable(host string, port int) error {
+func CheckPortAvailable(host string, port int) error {
 	ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
 	if err != nil {
 		return err
@@ -105,7 +155,7 @@ func checkPortAvailable(host string, port int) error {
 	return nil
 }
 
-func checkPacketPortAvailable(host string, port int) error {
+func CheckPacketPortAvailable(host string, port int) error {
 	ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port)))
 	if err != nil {
 		return err
@@ -119,7 +169,7 @@ func checkPacketPortAvailable(host string, port int) error {
 }
 
 // check if error is "address already in use"
-func errorIsAddrInUse(err error) bool {
+func ErrorIsAddrInUse(err error) bool {
 	errOpError, ok := err.(*net.OpError)
 	if !ok {
 		return false
diff --git a/home/helpers_test.go b/util/network_utils_test.go
similarity index 52%
rename from home/helpers_test.go
rename to util/network_utils_test.go
index c2095966..7feac0f2 100644
--- a/home/helpers_test.go
+++ b/util/network_utils_test.go
@@ -1,14 +1,12 @@
-package home
+package util
 
 import (
+	"log"
 	"testing"
-
-	"github.com/AdguardTeam/golibs/log"
-	"github.com/stretchr/testify/assert"
 )
 
 func TestGetValidNetInterfacesForWeb(t *testing.T) {
-	ifaces, err := getValidNetInterfacesForWeb()
+	ifaces, err := GetValidNetInterfacesForWeb()
 	if err != nil {
 		t.Fatalf("Cannot get net interfaces: %s", err)
 	}
@@ -24,10 +22,3 @@ func TestGetValidNetInterfacesForWeb(t *testing.T) {
 		log.Printf("%v", iface)
 	}
 }
-
-func TestSplitNext(t *testing.T) {
-	s := " a,b , c "
-	assert.True(t, SplitNext(&s, ',') == "a")
-	assert.True(t, SplitNext(&s, ',') == "b")
-	assert.True(t, SplitNext(&s, ',') == "c" && len(s) == 0)
-}
diff --git a/home/os_freebsd.go b/util/os_freebsd.go
similarity index 86%
rename from home/os_freebsd.go
rename to util/os_freebsd.go
index 43ee223e..33311e16 100644
--- a/home/os_freebsd.go
+++ b/util/os_freebsd.go
@@ -1,6 +1,6 @@
 // +build freebsd
 
-package home
+package util
 
 import (
 	"os"
@@ -11,7 +11,7 @@ import (
 
 // Set user-specified limit of how many fd's we can use
 // https://github.com/AdguardTeam/AdGuardHome/issues/659
-func setRlimit(val uint) {
+func SetRlimit(val uint) {
 	var rlim syscall.Rlimit
 	rlim.Max = int64(val)
 	rlim.Cur = int64(val)
@@ -22,6 +22,6 @@ func setRlimit(val uint) {
 }
 
 // Check if the current user has root (administrator) rights
-func haveAdminRights() (bool, error) {
+func HaveAdminRights() (bool, error) {
 	return os.Getuid() == 0, nil
 }
diff --git a/home/os_unix.go b/util/os_unix.go
similarity index 87%
rename from home/os_unix.go
rename to util/os_unix.go
index 2623376e..338edfa8 100644
--- a/home/os_unix.go
+++ b/util/os_unix.go
@@ -1,6 +1,6 @@
 // +build aix darwin dragonfly linux netbsd openbsd solaris
 
-package home
+package util
 
 import (
 	"os"
@@ -11,7 +11,7 @@ import (
 
 // Set user-specified limit of how many fd's we can use
 // https://github.com/AdguardTeam/AdGuardHome/issues/659
-func setRlimit(val uint) {
+func SetRlimit(val uint) {
 	var rlim syscall.Rlimit
 	rlim.Max = uint64(val)
 	rlim.Cur = uint64(val)
@@ -22,6 +22,6 @@ func setRlimit(val uint) {
 }
 
 // Check if the current user has root (administrator) rights
-func haveAdminRights() (bool, error) {
+func HaveAdminRights() (bool, error) {
 	return os.Getuid() == 0, nil
 }
diff --git a/home/os_windows.go b/util/os_windows.go
similarity index 87%
rename from home/os_windows.go
rename to util/os_windows.go
index f6949d93..e081f758 100644
--- a/home/os_windows.go
+++ b/util/os_windows.go
@@ -1,12 +1,12 @@
-package home
+package util
 
 import "golang.org/x/sys/windows"
 
 // Set user-specified limit of how many fd's we can use
-func setRlimit(val uint) {
+func SetRlimit(val uint) {
 }
 
-func haveAdminRights() (bool, error) {
+func HaveAdminRights() (bool, error) {
 	var token windows.Token
 	h, _ := windows.GetCurrentProcess()
 	err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)
diff --git a/home/syslog_others.go b/util/syslog_others.go
similarity index 62%
rename from home/syslog_others.go
rename to util/syslog_others.go
index 8aa0f8b0..f4ad9119 100644
--- a/home/syslog_others.go
+++ b/util/syslog_others.go
@@ -1,14 +1,14 @@
 // +build !windows,!nacl,!plan9
 
-package home
+package util
 
 import (
 	"log"
 	"log/syslog"
 )
 
-// configureSyslog reroutes standard logger output to syslog
-func configureSyslog() error {
+// ConfigureSyslog reroutes standard logger output to syslog
+func ConfigureSyslog(serviceName string) error {
 	w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
 	if err != nil {
 		return err
diff --git a/home/syslog_windows.go b/util/syslog_windows.go
similarity index 94%
rename from home/syslog_windows.go
rename to util/syslog_windows.go
index a80933bb..30ee7815 100644
--- a/home/syslog_windows.go
+++ b/util/syslog_windows.go
@@ -1,4 +1,4 @@
-package home
+package util
 
 import (
 	"log"
@@ -17,7 +17,7 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
 	return len(b), w.el.Info(1, string(b))
 }
 
-func configureSyslog() error {
+func ConfigureSyslog(serviceName string) error {
 	// Note that the eventlog src is the same as the service name
 	// Otherwise, we will get "the description for event id cannot be found" warning in every log record