From 7a3eda02cec158e911c1dbc8ddcb7cb13ed4836b Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Thu, 13 Feb 2020 18:42:07 +0300
Subject: [PATCH] Fix #1069 install: check static ip

Squashed commit of the following:

commit 57466233cbeb89aff82d8610778f7c3b60fe8426
Merge: 2df5f281 867bf545
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 18:39:15 2020 +0300

    Merge branch 'master' into 1069-install-static-ip

commit 2df5f281c4f5949b92edd4747ece60ff73799e54
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 18:35:54 2020 +0300

    *: lang fix

commit b4649a6b2781741979531faf862b88c2557f1445
Merge: c2785253 f61d5f0f
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 16:47:30 2020 +0300

    *(home): fixed issues with setting static IP on Mac

commit c27852537d2f5ce62b16c43f4241a15d0fb8c9fd
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 14:14:30 2020 +0300

    +(dhcpd): added static IP for MacOS

commit f61d5f0f85a954120b2676a5153f10a05662cf42
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 14:13:35 2020 +0300

    + client: show confirm before setting static IP

commit 7afa16fbe76dff4485d166f6164bae171e0110c9
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 13:51:52 2020 +0300

    - client: fix text

commit 019bff0851c584302fa44317fc748b3319be9470
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 13:49:16 2020 +0300

    - client: pass all params to the check_config request

commit 194bed72f567ae815cbd424e2df1ac5be65e0c02
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 17:12:16 2020 +0300

    *: fix home_test

commit 9359f6b55f5e36dd311fb85b6a83bb6227308f03
Merge: ae299058 c5ca2a77
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:54:54 2020 +0300

    Merge with master

commit ae2990582defd8062b99c546b2a932a8ba06c35d
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:53:36 2020 +0300

    *(global): refactoring - moved runtime properties to Context

commit d8d48c53869a94d18c5ea7bcf78613e83b24bfd8
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:04:25 2020 +0300

    *(dhcpd): refactoring, use dhcpd/network_utils where possible

commit 8d039c572f0e5f5245bd155a4e4d35400e6962c6
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Feb 7 18:37:39 2020 +0300

    - client: fix button position

commit 26c47e59dd63317bdb959cb416e7c1c0bfdf7dc1
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Feb 7 18:08:56 2020 +0300

    - client: fix static ip description

commit cb12babc4698d048478570303af8955a35e8531d
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 17:08:39 2020 +0300

    *: lower log level for some commands

commit d9001ff84852d708e400d039503141929e06d774
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 16:17:59 2020 +0300

    *(documentation): updated openapi

commit 1d213d53c88d5009a4b1d33d4cfa9e215c644bec
Merge: 8406d7d2 80861860
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 15:16:46 2020 +0300

    *: merge with master

commit 8406d7d28827ce1ed9d9f6770ce1700681811535
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Jan 31 16:52:22 2020 +0300

    - client: fix locales

commit fb476b011768367be51010c89754dcd23b383f5a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:29:03 2020 +0300

    linter

commit 84b5708e71c88a9643d402ab630270f5e7bf35b8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:27:53 2020 +0300

    linter

commit 143a86a28a3465776f803f6b99b9f3c64b26400e
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:26:47 2020 +0300

    linter

... and 7 more commits
---
 AGHTechDoc.md                                 |  62 ++-
 client/src/__locales/en.json                  |  11 +-
 client/src/helpers/form.js                    |   7 +
 client/src/install/Setup/Settings.js          | 131 +++++-
 client/src/install/Setup/Setup.css            |   5 +
 client/src/install/Setup/index.js             |  31 +-
 client/src/reducers/install.js                |   8 +-
 dhcpd/dhcp_http.go                            | 175 +-------
 dhcpd/network_utils.go                        | 312 ++++++++++++++
 dhcpd/network_utils_test.go                   |  61 +++
 home/auth.go                                  |  18 +-
 home/auth_test.go                             |   6 +-
 home/config.go                                |  41 +-
 home/control.go                               | 127 +++++-
 home/control_filtering.go                     |   4 +-
 home/control_install.go                       | 108 ++++-
 home/control_tls.go                           |   6 +-
 home/control_update.go                        |  31 +-
 home/control_update_test.go                   |  19 +-
 home/dns.go                                   |  12 +-
 home/filter.go                                |   7 +-
 home/filter_test.go                           |   9 +-
 home/helpers.go                               | 380 ------------------
 home/home.go                                  | 159 ++++++--
 home/home_test.go                             |   9 +-
 home/service.go                               |   7 +-
 home/upgrade.go                               |  18 +-
 home/whois.go                                 |   4 +-
 openapi/openapi.yaml                          |  22 +
 util/helpers.go                               |  59 +++
 util/helpers_test.go                          |  14 +
 util/network_utils.go                         | 194 +++++++++
 .../network_utils_test.go                     |  15 +-
 {home => util}/os_freebsd.go                  |   6 +-
 {home => util}/os_unix.go                     |   6 +-
 {home => util}/os_windows.go                  |   6 +-
 {home => util}/syslog_others.go               |   6 +-
 {home => util}/syslog_windows.go              |   4 +-
 38 files changed, 1319 insertions(+), 781 deletions(-)
 create mode 100644 dhcpd/network_utils.go
 create mode 100644 dhcpd/network_utils_test.go
 delete mode 100644 home/helpers.go
 create mode 100644 util/helpers.go
 create mode 100644 util/helpers_test.go
 create mode 100644 util/network_utils.go
 rename home/helpers_test.go => util/network_utils_test.go (52%)
 rename {home => util}/os_freebsd.go (86%)
 rename {home => util}/os_unix.go (87%)
 rename {home => util}/os_windows.go (87%)
 rename {home => util}/syslog_others.go (62%)
 rename {home => util}/syslog_windows.go (94%)

diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index 5d812e6b..c5d09f8a 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -138,10 +138,13 @@ Request:
 	{
 	"web":{"port":80,"ip":"192.168.11.33"},
 	"dns":{"port":53,"ip":"127.0.0.1","autofix":false},
+	"set_static_ip": true | false
 	}
 
 Server should check whether a port is available only in case it itself isn't already listening on that port.
 
+If `set_static_ip` is `true`, Server attempts to set a static IP for the network interface chosen by `dns.ip` setting.  If the operation is successful, `static_ip.static` setting will be `yes`.  If it fails, `static_ip.static` setting will be set to `error` and `static_ip.error` will contain the error message.
+
 Server replies on success:
 
 	200 OK
@@ -149,7 +152,14 @@ Server replies on success:
 	{
 	"web":{"status":""},
 	"dns":{"status":""},
+	"static_ip": {
+		"static": "yes|no|error",
+		"ip": "<Current dynamic IP address>", // set if static=no
+		"error": "..." // set if static=error
 	}
+	}
+
+If `static_ip.static` is `no`, Server has detected that the system uses a dynamic address and it can  automatically set a static address if `set_static_ip` in request is `true`.  See section `Static IP check/set` for detailed process.
 
 Server replies on error:
 
@@ -172,7 +182,11 @@ Request:
 	POST /control/install/check_config
 
 	{
-	"dns":{"port":53,"ip":"127.0.0.1","autofix":false}
+	"dns":{
+		"port":53,
+		"ip":"127.0.0.1",
+		"autofix":false
+	}
 	}
 
 Check if DNSStubListener is enabled:
@@ -499,13 +513,7 @@ which will print:
 	default via 192.168.0.1 proto dhcp metric 100
 
 
-#### Phase 2
-
-This method only works on Raspbian.
-
-On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`.  This must be configured in `/etc/netplan/01-netcfg.yaml`.
-
-Fedora doesn't use `dhcpcd.conf` configuration at all.
+#### Phase 2 (Raspbian)
 
 Step 1.
 
@@ -526,6 +534,44 @@ If we would set a different IP address, we'd need to replace the IP address for
 	ip addr replace dev eth0 192.168.0.1/24
 
 
+#### Phase 2 (Ubuntu)
+
+`/etc/netplan/01-netcfg.yaml` or `/etc/netplan/01-network-manager-all.yaml`
+
+This configuration example has a static IP set for `enp0s3` interface:
+
+	network:
+		version: 2
+		renderer: networkd
+		ethernets:
+			enp0s3:
+				dhcp4: no
+				addresses: [192.168.0.2/24]
+				gateway: 192.168.0.1
+				nameservers:
+					addresses: [192.168.0.1,8.8.8.8]
+
+For dynamic configuration `dhcp4: yes` is set.
+
+Make a backup copy to `/etc/netplan/01-netcfg.yaml.backup`.
+
+Apply:
+
+	netplan apply
+
+Restart network:
+
+	systemctl restart networking
+
+or:
+
+	systemctl restart network-manager
+
+or:
+
+	systemctl restart system-networkd
+
+
 ### Add a static lease
 
 Request:
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 961b47c7..ee17dbc1 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -458,9 +458,16 @@
     "check_reason": "Reason: {{reason}}",
     "check_rule": "Rule: {{rule}}",
     "check_service": "Service name: {{service}}",
-    "check_not_found": "Doesn't exist in any filter",
+    "check_not_found": "Not found in your filter lists",
     "client_confirm_block": "Are you sure you want to block the client \"{{ip}}\"?",
     "client_confirm_unblock": "Are you sure you want to unblock the client \"{{ip}}\"?",
     "client_blocked": "Client \"{{ip}}\" successfully blocked",
-    "client_unblocked": "Client \"{{ip}}\" successfully unblocked"
+    "client_unblocked": "Client \"{{ip}}\" successfully unblocked",
+    "static_ip": "Static IP Address",
+    "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 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?",
+    "confirm_static_ip": "AdGuard Home will configure {{ip}} to be your static IP address. Do you want to proceed?"
 }
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index 0c617ff5..7aed918c 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -240,6 +240,13 @@ export const port = (value) => {
     return undefined;
 };
 
+export const validInstallPort = (value) => {
+    if (value < 1 || value > 65535) {
+        return <Trans>form_error_port</Trans>;
+    }
+    return undefined;
+};
+
 export const portTLS = (value) => {
     if (value === 0) {
         return undefined;
diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js
index 246206ba..876aa05b 100644
--- a/client/src/install/Setup/Settings.js
+++ b/client/src/install/Setup/Settings.js
@@ -7,26 +7,17 @@ import flow from 'lodash/flow';
 
 import Controls from './Controls';
 import AddressList from './AddressList';
+
 import { getInterfaceIp } from '../../helpers/helpers';
 import { ALL_INTERFACES_IP } from '../../helpers/constants';
-import { renderInputField } from '../../helpers/form';
+import { renderInputField, required, validInstallPort, toNumber } from '../../helpers/form';
 
-const required = (value) => {
-    if (value || value === 0) {
-        return false;
-    }
-    return <Trans>form_error_required</Trans>;
+const STATIC_STATUS = {
+    ENABLED: 'yes',
+    DISABLED: 'no',
+    ERROR: 'error',
 };
 
-const port = (value) => {
-    if (value < 1 || value > 65535) {
-        return <Trans>form_error_port</Trans>;
-    }
-    return false;
-};
-
-const toNumber = value => value && parseInt(value, 10);
-
 const renderInterfaces = (interfaces => (
     Object.keys(interfaces).map((item) => {
         const option = interfaces[item];
@@ -79,11 +70,91 @@ class Settings extends Component {
         });
     }
 
+    getStaticIpMessage = (staticIp) => {
+        const { static: status, ip } = staticIp;
+
+        if (!status) {
+            return '';
+        }
+
+        return (
+            <Fragment>
+                {status === STATIC_STATUS.DISABLED && (
+                    <Fragment>
+                        <div className="mb-2">
+                            <Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
+                                install_static_configure
+                            </Trans>
+                        </div>
+                        <button
+                            type="button"
+                            className="btn btn-outline-primary btn-sm"
+                            onClick={() => this.handleStaticIp(ip)}
+                        >
+                            <Trans>set_static_ip</Trans>
+                        </button>
+                    </Fragment>
+                )}
+                {status === STATIC_STATUS.ERROR && (
+                    <div className="text-danger">
+                        <Trans>install_static_error</Trans>
+                    </div>
+                )}
+                {status === STATIC_STATUS.ENABLED && (
+                    <div className="text-success">
+                        <Trans>
+                            install_static_ok
+                        </Trans>
+                    </div>
+                )}
+            </Fragment>
+        );
+    };
+
+    handleAutofix = (type) => {
+        const {
+            webIp,
+            webPort,
+            dnsIp,
+            dnsPort,
+            handleFix,
+        } = this.props;
+
+        const web = { ip: webIp, port: webPort, autofix: false };
+        const dns = { ip: dnsIp, port: dnsPort, autofix: false };
+        const set_static_ip = false;
+
+        if (type === 'web') {
+            web.autofix = true;
+        } else {
+            dns.autofix = true;
+        }
+
+        handleFix(web, dns, set_static_ip);
+    };
+
+    handleStaticIp = (ip) => {
+        const {
+            webIp,
+            webPort,
+            dnsIp,
+            dnsPort,
+            handleFix,
+        } = this.props;
+
+        const web = { ip: webIp, port: webPort, autofix: false };
+        const dns = { ip: dnsIp, port: dnsPort, autofix: false };
+        const set_static_ip = true;
+
+        if (window.confirm(this.props.t('confirm_static_ip', { ip }))) {
+            handleFix(web, dns, set_static_ip);
+        }
+    };
+
     render() {
         const {
             handleSubmit,
             handleChange,
-            handleAutofix,
             webIp,
             webPort,
             dnsIp,
@@ -100,6 +171,7 @@ class Settings extends Component {
             status: dnsStatus,
             can_autofix: isDnsFixAvailable,
         } = config.dns;
+        const { staticIp } = config;
 
         return (
             <form className="setup__step" onSubmit={handleSubmit}>
@@ -137,7 +209,7 @@ class Settings extends Component {
                                     type="number"
                                     className="form-control"
                                     placeholder="80"
-                                    validate={[port, required]}
+                                    validate={[validInstallPort, required]}
                                     normalize={toNumber}
                                     onChange={handleChange}
                                 />
@@ -151,11 +223,12 @@ class Settings extends Component {
                                         <button
                                             type="button"
                                             className="btn btn-secondary btn-sm ml-2"
-                                            onClick={() => handleAutofix('web', webIp, webPort)}
+                                            onClick={() => this.handleAutofix('web')}
                                         >
                                             <Trans>fix</Trans>
                                         </button>
                                     }
+                                    <hr className="divider--small" />
                                 </div>
                             }
                         </div>
@@ -171,6 +244,7 @@ class Settings extends Component {
                         </div>
                     </div>
                 </div>
+
                 <div className="setup__group">
                     <div className="setup__subtitle">
                         <Trans>install_settings_dns</Trans>
@@ -205,7 +279,7 @@ class Settings extends Component {
                                     type="number"
                                     className="form-control"
                                     placeholder="80"
-                                    validate={[port, required]}
+                                    validate={[validInstallPort, required]}
                                     normalize={toNumber}
                                     onChange={handleChange}
                                 />
@@ -220,7 +294,7 @@ class Settings extends Component {
                                             <button
                                                 type="button"
                                                 className="btn btn-secondary btn-sm ml-2"
-                                                onClick={() => handleAutofix('dns', dnsIp, dnsPort)}
+                                                onClick={() => this.handleAutofix('dns')}
                                             >
                                                 <Trans>fix</Trans>
                                             </button>
@@ -237,6 +311,7 @@ class Settings extends Component {
                                             <Trans>autofix_warning_result</Trans>
                                         </p>
                                     </div>
+                                    <hr className="divider--small" />
                                 </Fragment>
                             }
                         </div>
@@ -253,6 +328,19 @@ class Settings extends Component {
                         </div>
                     </div>
                 </div>
+
+                <div className="setup__group">
+                    <div className="setup__subtitle">
+                        <Trans>static_ip</Trans>
+                    </div>
+
+                    <div className="mb-2">
+                        <Trans>static_ip_desc</Trans>
+                    </div>
+
+                    {this.getStaticIpMessage(staticIp)}
+                </div>
+
                 <Controls invalid={invalid} />
             </form>
         );
@@ -262,7 +350,7 @@ class Settings extends Component {
 Settings.propTypes = {
     handleSubmit: PropTypes.func.isRequired,
     handleChange: PropTypes.func,
-    handleAutofix: PropTypes.func,
+    handleFix: PropTypes.func.isRequired,
     validateForm: PropTypes.func,
     webIp: PropTypes.string.isRequired,
     dnsIp: PropTypes.string.isRequired,
@@ -278,6 +366,7 @@ Settings.propTypes = {
     interfaces: PropTypes.object.isRequired,
     invalid: PropTypes.bool.isRequired,
     initialValues: PropTypes.object,
+    t: PropTypes.func.isRequired,
 };
 
 const selector = formValueSelector('install');
diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css
index 11ee1430..aac7ea0e 100644
--- a/client/src/install/Setup/Setup.css
+++ b/client/src/install/Setup/Setup.css
@@ -119,3 +119,8 @@
 .setup__error {
     margin: -5px 0 5px;
 }
+
+.divider--small {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+}
diff --git a/client/src/install/Setup/index.js b/client/src/install/Setup/index.js
index ca91cd8f..82d8f84b 100644
--- a/client/src/install/Setup/index.js
+++ b/client/src/install/Setup/index.js
@@ -33,31 +33,19 @@ class Setup extends Component {
     }
 
     handleFormSubmit = (values) => {
-        this.props.setAllSettings(values);
+        const { staticIp, ...config } = values;
+        this.props.setAllSettings(config);
     };
 
     handleFormChange = debounce((values) => {
-        if (values && values.web.port && values.dns.port) {
-            this.props.checkConfig(values);
+        const { web, dns } = values;
+        if (values && web.port && dns.port) {
+            this.props.checkConfig({ web, dns, set_static_ip: false });
         }
     }, DEBOUNCE_TIMEOUT);
 
-    handleAutofix = (type, ip, port) => {
-        const data = {
-            ip,
-            port,
-            autofix: true,
-        };
-
-        if (type === 'web') {
-            this.props.checkConfig({
-                web: { ...data },
-            });
-        } else {
-            this.props.checkConfig({
-                dns: { ...data },
-            });
-        }
+    handleFix = (web, dns, set_static_ip) => {
+        this.props.checkConfig({ web, dns, set_static_ip });
     };
 
     openDashboard = (ip, port) => {
@@ -95,7 +83,7 @@ class Setup extends Component {
                         onSubmit={this.nextStep}
                         onChange={this.handleFormChange}
                         validateForm={this.handleFormChange}
-                        handleAutofix={this.handleAutofix}
+                        handleFix={this.handleFix}
                     />
                 );
             case 3:
@@ -117,6 +105,7 @@ class Setup extends Component {
             step,
             web,
             dns,
+            staticIp,
             interfaces,
         } = this.props.install;
 
@@ -128,7 +117,7 @@ class Setup extends Component {
                         <div className="setup">
                             <div className="setup__container">
                                 <img src={logo} className="setup__logo" alt="logo" />
-                                {this.renderPage(step, { web, dns }, interfaces)}
+                                {this.renderPage(step, { web, dns, staticIp }, interfaces)}
                                 <Progress step={step} />
                             </div>
                         </div>
diff --git a/client/src/reducers/install.js b/client/src/reducers/install.js
index 3709b0ec..b3f95dfb 100644
--- a/client/src/reducers/install.js
+++ b/client/src/reducers/install.js
@@ -32,9 +32,10 @@ const install = handleActions({
     [actions.checkConfigSuccess]: (state, { payload }) => {
         const web = { ...state.web, ...payload.web };
         const dns = { ...state.dns, ...payload.dns };
+        const staticIp = { ...state.staticIp, ...payload.static_ip };
 
         const newState = {
-            ...state, web, dns, processingCheck: false,
+            ...state, web, dns, staticIp, processingCheck: false,
         };
         return newState;
     },
@@ -55,6 +56,11 @@ const install = handleActions({
         status: '',
         can_autofix: false,
     },
+    staticIp: {
+        static: '',
+        ip: '',
+        error: '',
+    },
     interfaces: {},
 });
 
diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go
index e1b3d4fb..9105d76b 100644
--- a/dhcpd/dhcp_http.go
+++ b/dhcpd/dhcp_http.go
@@ -2,18 +2,16 @@ package dhcpd
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net"
 	"net/http"
 	"os"
-	"os/exec"
-	"runtime"
 	"strings"
 	"time"
 
-	"github.com/AdguardTeam/golibs/file"
+	"github.com/AdguardTeam/AdGuardHome/util"
+
 	"github.com/AdguardTeam/golibs/log"
 )
 
@@ -97,10 +95,9 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
 	s.conf.ConfigModified()
 
 	if newconfig.Enabled {
-
-		staticIP, err := hasStaticIP(newconfig.InterfaceName)
+		staticIP, err := HasStaticIP(newconfig.InterfaceName)
 		if !staticIP && err == nil {
-			err = setStaticIP(newconfig.InterfaceName)
+			err = SetStaticIP(newconfig.InterfaceName)
 			if err != nil {
 				httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
 				return
@@ -115,7 +112,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-type netInterface struct {
+type netInterfaceJSON struct {
 	Name         string   `json:"name"`
 	MTU          int      `json:"mtu"`
 	HardwareAddr string   `json:"hardware_address"`
@@ -123,33 +120,10 @@ type netInterface struct {
 	Flags        string   `json:"flags"`
 }
 
-// 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
-}
-
 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
@@ -170,7 +144,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		jsonIface := netInterface{
+		jsonIface := netInterfaceJSON{
 			Name:         iface.Name,
 			MTU:          iface.MTU,
 			HardwareAddr: iface.HardwareAddr.String(),
@@ -240,14 +214,14 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
 	othSrv["found"] = foundVal
 
 	staticIP := map[string]interface{}{}
-	isStaticIP, err := hasStaticIP(interfaceName)
+	isStaticIP, err := HasStaticIP(interfaceName)
 	staticIPStatus := "yes"
 	if err != nil {
 		staticIPStatus = "error"
 		staticIP["error"] = err.Error()
 	} else if !isStaticIP {
 		staticIPStatus = "no"
-		staticIP["ip"] = getFullIP(interfaceName)
+		staticIP["ip"] = util.GetSubnet(interfaceName)
 	}
 	staticIP["static"] = staticIPStatus
 
@@ -263,137 +237,6 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
 	}
 }
 
-// Check if network interface has a static IP configured
-func hasStaticIP(ifaceName string) (bool, error) {
-	if runtime.GOOS == "windows" {
-		return false, errors.New("Can't detect static IP: not supported on Windows")
-	}
-
-	body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
-	if err != nil {
-		return false, err
-	}
-	lines := strings.Split(string(body), "\n")
-	nameLine := fmt.Sprintf("interface %s", ifaceName)
-	withinInterfaceCtx := false
-
-	for _, line := range lines {
-		line = strings.TrimSpace(line)
-
-		if withinInterfaceCtx && len(line) == 0 {
-			// an empty line resets our state
-			withinInterfaceCtx = false
-		}
-
-		if len(line) == 0 || line[0] == '#' {
-			continue
-		}
-		line = strings.TrimSpace(line)
-
-		if !withinInterfaceCtx {
-			if line == nameLine {
-				// we found our interface
-				withinInterfaceCtx = true
-			}
-
-		} else {
-			if strings.HasPrefix(line, "interface ") {
-				// we found another interface - reset our state
-				withinInterfaceCtx = false
-				continue
-			}
-			if strings.HasPrefix(line, "static ip_address=") {
-				return true, nil
-			}
-		}
-	}
-
-	return false, nil
-}
-
-// 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]
-}
-
-// Get gateway IP address
-func getGatewayIP(ifaceName string) string {
-	cmd := exec.Command("ip", "route", "show", "dev", 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) < 3 || fields[0] != "default" {
-		return ""
-	}
-
-	ip := net.ParseIP(fields[2])
-	if ip == nil {
-		return ""
-	}
-
-	return fields[2]
-}
-
-// Set a static IP for network interface
-func setStaticIP(ifaceName string) error {
-	ip := getFullIP(ifaceName)
-	if len(ip) == 0 {
-		return errors.New("Can't get IP address")
-	}
-
-	body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
-	if err != nil {
-		return err
-	}
-
-	ip4, _, err := net.ParseCIDR(ip)
-	if err != nil {
-		return err
-	}
-
-	add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
-		ifaceName, ip)
-	body = append(body, []byte(add)...)
-
-	gatewayIP := getGatewayIP(ifaceName)
-	if len(gatewayIP) != 0 {
-		add = fmt.Sprintf("static routers=%s\n",
-			gatewayIP)
-		body = append(body, []byte(add)...)
-	}
-
-	add = fmt.Sprintf("static domain_name_servers=%s\n\n",
-		ip4)
-	body = append(body, []byte(add)...)
-
-	err = file.SafeWrite("/etc/dhcpcd.conf", body)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
 
 	lj := staticLeaseJSON{}
diff --git a/dhcpd/network_utils.go b/dhcpd/network_utils.go
new file mode 100644
index 00000000..79c13285
--- /dev/null
+++ b/dhcpd/network_utils.go
@@ -0,0 +1,312 @@
+package dhcpd
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os/exec"
+	"regexp"
+	"runtime"
+	"strings"
+
+	"github.com/AdguardTeam/AdGuardHome/util"
+
+	"github.com/AdguardTeam/golibs/file"
+
+	"github.com/AdguardTeam/golibs/log"
+)
+
+// Check if network interface has a static IP configured
+// Supports: Raspbian.
+func HasStaticIP(ifaceName string) (bool, error) {
+	if runtime.GOOS == "linux" {
+		body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
+		if err != nil {
+			return false, err
+		}
+
+		return hasStaticIPDhcpcdConf(string(body), ifaceName), nil
+	}
+
+	if runtime.GOOS == "darwin" {
+		return hasStaticIPDarwin(ifaceName)
+	}
+
+	return false, fmt.Errorf("Cannot check if IP is static: not supported on %s", runtime.GOOS)
+}
+
+// Set a static IP for the specified network interface
+func SetStaticIP(ifaceName string) error {
+	if runtime.GOOS == "linux" {
+		return setStaticIPDhcpdConf(ifaceName)
+	}
+
+	if runtime.GOOS == "darwin" {
+		return setStaticIPDarwin(ifaceName)
+	}
+
+	return fmt.Errorf("Cannot set static IP on %s", runtime.GOOS)
+}
+
+// for dhcpcd.conf
+func hasStaticIPDhcpcdConf(dhcpConf, ifaceName string) bool {
+	lines := strings.Split(dhcpConf, "\n")
+	nameLine := fmt.Sprintf("interface %s", ifaceName)
+	withinInterfaceCtx := false
+
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+
+		if withinInterfaceCtx && len(line) == 0 {
+			// an empty line resets our state
+			withinInterfaceCtx = false
+		}
+
+		if len(line) == 0 || line[0] == '#' {
+			continue
+		}
+		line = strings.TrimSpace(line)
+
+		if !withinInterfaceCtx {
+			if line == nameLine {
+				// we found our interface
+				withinInterfaceCtx = true
+			}
+
+		} else {
+			if strings.HasPrefix(line, "interface ") {
+				// we found another interface - reset our state
+				withinInterfaceCtx = false
+				continue
+			}
+			if strings.HasPrefix(line, "static ip_address=") {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// Get gateway IP address
+func getGatewayIP(ifaceName string) string {
+	cmd := exec.Command("ip", "route", "show", "dev", 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) < 3 || fields[0] != "default" {
+		return ""
+	}
+
+	ip := net.ParseIP(fields[2])
+	if ip == nil {
+		return ""
+	}
+
+	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 updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
+	var body []byte
+
+	add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
+		ifaceName, ip)
+	body = append(body, []byte(add)...)
+
+	if len(gatewayIP) != 0 {
+		add = fmt.Sprintf("static routers=%s\n",
+			gatewayIP)
+		body = append(body, []byte(add)...)
+	}
+
+	add = fmt.Sprintf("static domain_name_servers=%s\n\n",
+		dnsIP)
+	body = append(body, []byte(add)...)
+
+	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", portInfo.name)
+	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_test.go b/dhcpd/network_utils_test.go
new file mode 100644
index 00000000..2957a411
--- /dev/null
+++ b/dhcpd/network_utils_test.go
@@ -0,0 +1,61 @@
+package dhcpd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHasStaticIPDhcpcdConf(t *testing.T) {
+	dhcpdConf := `#comment
+# comment
+
+interface eth0
+static ip_address=192.168.0.1/24
+
+# interface wlan0
+static ip_address=192.168.1.1/24
+
+# comment
+`
+	assert.True(t, !hasStaticIPDhcpcdConf(dhcpdConf, "wlan0"))
+
+	dhcpdConf = `#comment
+# comment
+
+interface eth0
+static ip_address=192.168.0.1/24
+
+# interface wlan0
+static ip_address=192.168.1.1/24
+
+# comment
+
+interface wlan0
+# comment
+static ip_address=192.168.2.1/24
+`
+	assert.True(t, hasStaticIPDhcpcdConf(dhcpdConf, "wlan0"))
+}
+
+func TestSetStaticIPDhcpcdConf(t *testing.T) {
+	dhcpcdConf := `
+interface wlan0
+static ip_address=192.168.0.2/24
+static routers=192.168.0.1
+static domain_name_servers=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
+	dhcpcdConf = `
+interface wlan0
+static ip_address=192.168.0.2/24
+static domain_name_servers=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/auth.go b/home/auth.go
index 9afe2c87..e6e4642a 100644
--- a/home/auth.go
+++ b/home/auth.go
@@ -152,7 +152,7 @@ func (a *Auth) addSession(data []byte, s *session) {
 	a.sessions[name] = s
 	a.lock.Unlock()
 	if a.storeSession(data, s) {
-		log.Info("Auth: created session %s: expire=%d", name, s.expire)
+		log.Debug("Auth: created session %s: expire=%d", name, s.expire)
 	}
 }
 
@@ -307,7 +307,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	cookie := config.auth.httpCookie(req)
+	cookie := Context.auth.httpCookie(req)
 	if len(cookie) == 0 {
 		log.Info("Auth: invalid user name or password: name='%s'", req.Name)
 		time.Sleep(1 * time.Second)
@@ -328,7 +328,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
 	cookie := r.Header.Get("Cookie")
 	sess := parseCookie(cookie)
 
-	config.auth.RemoveSession(sess)
+	Context.auth.RemoveSession(sess)
 
 	w.Header().Set("Location", "/login.html")
 
@@ -365,10 +365,10 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
 
 		if r.URL.Path == "/login.html" {
 			// redirect to dashboard if already authenticated
-			authRequired := config.auth != nil && config.auth.AuthRequired()
+			authRequired := Context.auth != nil && Context.auth.AuthRequired()
 			cookie, err := r.Cookie(sessionCookieName)
 			if authRequired && err == nil {
-				r := config.auth.CheckSession(cookie.Value)
+				r := Context.auth.CheckSession(cookie.Value)
 				if r == 0 {
 					w.Header().Set("Location", "/")
 					w.WriteHeader(http.StatusFound)
@@ -383,12 +383,12 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
 			strings.HasPrefix(r.URL.Path, "/__locales/") {
 			// process as usual
 
-		} else if config.auth != nil && config.auth.AuthRequired() {
+		} else if Context.auth != nil && Context.auth.AuthRequired() {
 			// redirect to login page if not authenticated
 			ok := false
 			cookie, err := r.Cookie(sessionCookieName)
 			if err == nil {
-				r := config.auth.CheckSession(cookie.Value)
+				r := Context.auth.CheckSession(cookie.Value)
 				if r == 0 {
 					ok = true
 				} else if r < 0 {
@@ -398,7 +398,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
 				// there's no Cookie, check Basic authentication
 				user, pass, ok2 := r.BasicAuth()
 				if ok2 {
-					u := config.auth.UserFind(user, pass)
+					u := Context.auth.UserFind(user, pass)
 					if len(u.Name) != 0 {
 						ok = true
 					} else {
@@ -474,7 +474,7 @@ func (a *Auth) GetCurrentUser(r *http.Request) User {
 		// there's no Cookie, check Basic authentication
 		user, pass, ok := r.BasicAuth()
 		if ok {
-			u := config.auth.UserFind(user, pass)
+			u := Context.auth.UserFind(user, pass)
 			return u
 		}
 		return User{}
diff --git a/home/auth_test.go b/home/auth_test.go
index 19cd5001..38f826ec 100644
--- a/home/auth_test.go
+++ b/home/auth_test.go
@@ -100,7 +100,7 @@ func TestAuthHTTP(t *testing.T) {
 	users := []User{
 		User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
 	}
-	config.auth = InitAuth(fn, users, 60)
+	Context.auth = InitAuth(fn, users, 60)
 
 	handlerCalled := false
 	handler := func(w http.ResponseWriter, r *http.Request) {
@@ -129,7 +129,7 @@ func TestAuthHTTP(t *testing.T) {
 	assert.True(t, handlerCalled)
 
 	// perform login
-	cookie := config.auth.httpCookie(loginJSON{Name: "name", Password: "password"})
+	cookie := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"})
 	assert.True(t, cookie != "")
 
 	// get /
@@ -173,5 +173,5 @@ func TestAuthHTTP(t *testing.T) {
 	assert.True(t, handlerCalled)
 	r.Header.Del("Cookie")
 
-	config.auth.Close()
+	Context.auth.Close()
 }
diff --git a/home/config.go b/home/config.go
index 693c4a40..ab875fdc 100644
--- a/home/config.go
+++ b/home/config.go
@@ -44,19 +44,6 @@ type configuration struct {
 	// It's reset after config is parsed
 	fileData []byte
 
-	ourConfigFilename string // Config filename (can be overridden via the command line arguments)
-	ourWorkingDir     string // Location of our directory, used to protect against CWD being somewhere else
-	firstRun          bool   // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
-	pidFileName       string // PID file name.  Empty if no PID file was created.
-	// runningAsService flag is set to true when options are passed from the service runner
-	runningAsService bool
-	disableUpdate    bool // If set, don't check for updates
-	appSignalChannel chan os.Signal
-	controlLock      sync.Mutex
-	transport        *http.Transport
-	client           *http.Client
-	auth             *Auth // HTTP authentication module
-
 	// cached version.json to avoid hammering github.io for each page reload
 	versionCheckJSON     []byte
 	versionCheckLastTime time.Time
@@ -152,9 +139,8 @@ type tlsConfig struct {
 
 // initialize to default values, will be changed later when reading config or parsing command line
 var config = configuration{
-	ourConfigFilename: "AdGuardHome.yaml",
-	BindPort:          3000,
-	BindHost:          "0.0.0.0",
+	BindPort: 3000,
+	BindHost: "0.0.0.0",
 	DNS: dnsConfig{
 		BindHost:      "0.0.0.0",
 		Port:          53,
@@ -185,14 +171,6 @@ var config = configuration{
 
 // initConfig initializes default configuration for the current OS&ARCH
 func initConfig() {
-	config.transport = &http.Transport{
-		DialContext: customDialContext,
-	}
-	config.client = &http.Client{
-		Timeout:   time.Minute * 5,
-		Transport: config.transport,
-	}
-
 	config.WebSessionTTLHours = 30 * 24
 
 	config.DNS.QueryLogEnabled = true
@@ -209,24 +187,19 @@ func initConfig() {
 
 // getConfigFilename returns path to the current config file
 func (c *configuration) getConfigFilename() string {
-	configFile, err := filepath.EvalSymlinks(config.ourConfigFilename)
+	configFile, err := filepath.EvalSymlinks(Context.configFilename)
 	if err != nil {
 		if !os.IsNotExist(err) {
 			log.Error("unexpected error while config file path evaluation: %s", err)
 		}
-		configFile = config.ourConfigFilename
+		configFile = Context.configFilename
 	}
 	if !filepath.IsAbs(configFile) {
-		configFile = filepath.Join(config.ourWorkingDir, configFile)
+		configFile = filepath.Join(Context.workDir, configFile)
 	}
 	return configFile
 }
 
-// getDataDir returns path to the directory where we store databases and filters
-func (c *configuration) getDataDir() string {
-	return filepath.Join(c.ourWorkingDir, dataDir)
-}
-
 // getLogSettings reads logging settings from the config file.
 // we do it in a separate method in order to configure logger before the actual configuration is parsed and applied.
 func getLogSettings() logSettings {
@@ -292,8 +265,8 @@ func (c *configuration) write() error {
 
 	Context.clients.WriteDiskConfig(&config.Clients)
 
-	if config.auth != nil {
-		config.Users = config.auth.GetUsers()
+	if Context.auth != nil {
+		config.Users = Context.auth.GetUsers()
 	}
 
 	if Context.stats != nil {
diff --git a/home/control.go b/home/control.go
index 87247190..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)
 	}
@@ -129,7 +133,7 @@ type profileJSON struct {
 
 func handleGetProfile(w http.ResponseWriter, r *http.Request) {
 	pj := profileJSON{}
-	u := config.auth.GetCurrentUser(r)
+	u := Context.auth.GetCurrentUser(r)
 	pj.Name = u.Name
 
 	data, err := json.Marshal(pj)
@@ -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_filtering.go b/home/control_filtering.go
index 77c6cafa..8846b980 100644
--- a/home/control_filtering.go
+++ b/home/control_filtering.go
@@ -210,9 +210,9 @@ func handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
 }
 
 func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
-	config.controlLock.Unlock()
+	Context.controlLock.Unlock()
 	nUpdated, err := refreshFilters()
-	config.controlLock.Lock()
+	Context.controlLock.Lock()
 	if err != nil {
 		httpError(w, http.StatusInternalServerError, "%s", err)
 		return
diff --git a/home/control_install.go b/home/control_install.go
index 5311c091..17ffafa9 100644
--- a/home/control_install.go
+++ b/home/control_install.go
@@ -13,6 +13,10 @@ import (
 	"runtime"
 	"strconv"
 
+	"github.com/AdguardTeam/AdGuardHome/util"
+
+	"github.com/AdguardTeam/AdGuardHome/dhcpd"
+
 	"github.com/AdguardTeam/golibs/log"
 )
 
@@ -22,13 +26,21 @@ type firstRunData struct {
 	Interfaces map[string]interface{} `json:"interfaces"`
 }
 
+type netInterfaceJSON struct {
+	Name         string   `json:"name"`
+	MTU          int      `json:"mtu"`
+	HardwareAddr string   `json:"hardware_address"`
+	Addresses    []string `json:"ip_addresses"`
+	Flags        string   `json:"flags"`
+}
+
 // Get initial installation settings
 func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
 	data := firstRunData{}
 	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
@@ -36,7 +48,14 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
 
 	data.Interfaces = make(map[string]interface{})
 	for _, iface := range ifaces {
-		data.Interfaces[iface.Name] = iface
+		ifaceJSON := netInterfaceJSON{
+			Name:         iface.Name,
+			MTU:          iface.MTU,
+			HardwareAddr: iface.HardwareAddr,
+			Addresses:    iface.Addresses,
+			Flags:        iface.Flags,
+		}
+		data.Interfaces[iface.Name] = ifaceJSON
 	}
 
 	w.Header().Set("Content-Type", "application/json")
@@ -53,17 +72,24 @@ type checkConfigReqEnt struct {
 	Autofix bool   `json:"autofix"`
 }
 type checkConfigReq struct {
-	Web checkConfigReqEnt `json:"web"`
-	DNS checkConfigReqEnt `json:"dns"`
+	Web         checkConfigReqEnt `json:"web"`
+	DNS         checkConfigReqEnt `json:"dns"`
+	SetStaticIP bool              `json:"set_static_ip"`
 }
 
 type checkConfigRespEnt struct {
 	Status     string `json:"status"`
 	CanAutofix bool   `json:"can_autofix"`
 }
+type staticIPJSON struct {
+	Static string `json:"static"`
+	IP     string `json:"ip"`
+	Error  string `json:"error"`
+}
 type checkConfigResp struct {
-	Web checkConfigRespEnt `json:"web"`
-	DNS checkConfigRespEnt `json:"dns"`
+	Web      checkConfigRespEnt `json:"web"`
+	DNS      checkConfigRespEnt `json:"dns"`
+	StaticIP staticIPJSON       `json:"static_ip"`
 }
 
 // Check if ports are available, respond with results
@@ -77,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 {
 
@@ -95,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
 			}
 
@@ -103,11 +129,13 @@ 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 {
+			respData.StaticIP = handleStaticIP(reqData.DNS.IP, reqData.SetStaticIP)
 		}
 	}
 
@@ -119,6 +147,46 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// handleStaticIP - handles static IP request
+// It either checks if we have a static IP
+// Or if set=true, it tries to set it
+func handleStaticIP(ip string, set bool) staticIPJSON {
+	resp := staticIPJSON{}
+
+	interfaceName := util.GetInterfaceByIP(ip)
+	resp.Static = "no"
+
+	if len(interfaceName) == 0 {
+		resp.Static = "error"
+		resp.Error = fmt.Sprintf("Couldn't find network interface by IP %s", ip)
+		return resp
+	}
+
+	if set {
+		// Try to set static IP for the specified interface
+		err := dhcpd.SetStaticIP(interfaceName)
+		if err != nil {
+			resp.Static = "error"
+			resp.Error = err.Error()
+			return resp
+		}
+	}
+
+	// Fallthrough here even if we set static IP
+	// Check if we have a static IP and return the details
+	isStaticIP, err := dhcpd.HasStaticIP(interfaceName)
+	if err != nil {
+		resp.Static = "error"
+		resp.Error = err.Error()
+	} else {
+		if isStaticIP {
+			resp.Static = "yes"
+		}
+		resp.IP = util.GetSubnet(interfaceName)
+	}
+	return resp
+}
+
 // Check if DNSStubListener is active
 func checkDNSStubListener() bool {
 	if runtime.GOOS != "linux" {
@@ -129,7 +197,7 @@ func checkDNSStubListener() bool {
 	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
 	_, err := cmd.Output()
 	if err != nil || cmd.ProcessState.ExitCode() != 0 {
-		log.Error("command %s has failed: %v code:%d",
+		log.Info("command %s has failed: %v code:%d",
 			cmd.Path, err, cmd.ProcessState.ExitCode())
 		return false
 	}
@@ -138,7 +206,7 @@ func checkDNSStubListener() bool {
 	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
 	_, err = cmd.Output()
 	if err != nil || cmd.ProcessState.ExitCode() != 0 {
-		log.Error("command %s has failed: %v code:%d",
+		log.Info("command %s has failed: %v code:%d",
 			cmd.Path, err, cmd.ProcessState.ExitCode())
 		return false
 	}
@@ -228,7 +296,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)
@@ -236,13 +304,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
@@ -251,7 +319,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 	var curConfig configuration
 	copyInstallSettings(&curConfig, &config)
 
-	config.firstRun = false
+	Context.firstRun = false
 	config.BindHost = newSettings.Web.IP
 	config.BindPort = newSettings.Web.Port
 	config.DNS.BindHost = newSettings.DNS.IP
@@ -266,7 +334,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 	if err != nil || err2 != nil {
-		config.firstRun = true
+		Context.firstRun = true
 		copyInstallSettings(&config, &curConfig)
 		if err != nil {
 			httpError(w, http.StatusInternalServerError, "Couldn't initialize DNS server: %s", err)
@@ -278,11 +346,11 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 
 	u := User{}
 	u.Name = newSettings.Username
-	config.auth.UserAdd(&u, newSettings.Password)
+	Context.auth.UserAdd(&u, newSettings.Password)
 
 	err = config.write()
 	if err != nil {
-		config.firstRun = true
+		Context.firstRun = true
 		copyInstallSettings(&config, &curConfig)
 		httpError(w, http.StatusInternalServerError, "Couldn't write config: %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 7864cfbb..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"
 )
 
@@ -64,7 +66,7 @@ type getVersionJSONRequest struct {
 // Get the latest available version from the Internet
 func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
 
-	if config.disableUpdate {
+	if Context.disableUpdate {
 		return
 	}
 
@@ -77,10 +79,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
 
 	now := time.Now()
 	if !req.RecheckNow {
-		config.controlLock.Lock()
+		Context.controlLock.Lock()
 		cached := now.Sub(config.versionCheckLastTime) <= versionCheckPeriod && len(config.versionCheckJSON) != 0
 		data := config.versionCheckJSON
-		config.controlLock.Unlock()
+		Context.controlLock.Unlock()
 
 		if cached {
 			log.Tracef("Returning cached data")
@@ -93,7 +95,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
 	var resp *http.Response
 	for i := 0; i != 3; i++ {
 		log.Tracef("Downloading data from %s", versionCheckURL)
-		resp, err = config.client.Get(versionCheckURL)
+		resp, err = Context.client.Get(versionCheckURL)
 		if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") {
 			// This case may happen while we're restarting DNS server
 			// https://github.com/AdguardTeam/AdGuardHome/issues/934
@@ -116,10 +118,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	config.controlLock.Lock()
+	Context.controlLock.Lock()
 	config.versionCheckLastTime = now
 	config.versionCheckJSON = body
-	config.controlLock.Unlock()
+	Context.controlLock.Unlock()
 
 	w.Header().Set("Content-Type", "application/json")
 	_, err = w.Write(getVersionResp(body))
@@ -158,7 +160,7 @@ type updateInfo struct {
 func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
 	var u updateInfo
 
-	workDir := config.ourWorkingDir
+	workDir := Context.workDir
 
 	versionJSON := make(map[string]interface{})
 	err := json.Unmarshal(jsonData, &versionJSON)
@@ -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)
@@ -365,7 +367,7 @@ func copySupportingFiles(files []string, srcdir, dstdir string, useSrcNameOnly,
 
 // Download package file and save it to disk
 func getPackageFile(u *updateInfo) error {
-	resp, err := config.client.Get(u.pkgURL)
+	resp, err := Context.client.Get(u.pkgURL)
 	if err != nil {
 		return fmt.Errorf("HTTP request failed: %s", err)
 	}
@@ -436,17 +438,17 @@ func doUpdate(u *updateInfo) error {
 	}
 
 	// ./README.md -> backup/README.md
-	err = copySupportingFiles(files, config.ourWorkingDir, u.backupDir, true, true)
+	err = copySupportingFiles(files, Context.workDir, u.backupDir, true, true)
 	if err != nil {
 		return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
-			config.ourWorkingDir, u.backupDir, err)
+			Context.workDir, u.backupDir, err)
 	}
 
 	// update/[AdGuardHome/]README.md -> ./README.md
-	err = copySupportingFiles(files, u.updateDir, config.ourWorkingDir, false, true)
+	err = copySupportingFiles(files, u.updateDir, Context.workDir, false, true)
 	if err != nil {
 		return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
-			u.updateDir, config.ourWorkingDir, err)
+			u.updateDir, Context.workDir, err)
 	}
 
 	log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName)
@@ -478,8 +480,7 @@ func finishUpdate(u *updateInfo) {
 	cleanupAlways()
 
 	if runtime.GOOS == "windows" {
-
-		if config.runningAsService {
+		if Context.runningAsService {
 			// Note:
 			// we can't restart the service via "kardianos/service" package - it kills the process first
 			// we can't start a new instance - Windows doesn't allow it
diff --git a/home/control_update_test.go b/home/control_update_test.go
index c30a72e4..6ec4a186 100644
--- a/home/control_update_test.go
+++ b/home/control_update_test.go
@@ -8,9 +8,8 @@ import (
 )
 
 func TestDoUpdate(t *testing.T) {
-
 	config.DNS.Port = 0
-	config.ourWorkingDir = "..." // set absolute path
+	Context.workDir = "..." // set absolute path
 	newver := "v0.96"
 
 	data := `{
@@ -35,15 +34,15 @@ func TestDoUpdate(t *testing.T) {
 
 	u := updateInfo{
 		pkgURL:           "https://github.com/AdguardTeam/AdGuardHome/releases/download/" + newver + "/AdGuardHome_linux_amd64.tar.gz",
-		pkgName:          config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome_linux_amd64.tar.gz",
+		pkgName:          Context.workDir + "/agh-update-" + newver + "/AdGuardHome_linux_amd64.tar.gz",
 		newVer:           newver,
-		updateDir:        config.ourWorkingDir + "/agh-update-" + newver,
-		backupDir:        config.ourWorkingDir + "/agh-backup",
-		configName:       config.ourWorkingDir + "/AdGuardHome.yaml",
-		updateConfigName: config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome.yaml",
-		curBinName:       config.ourWorkingDir + "/AdGuardHome",
-		bkpBinName:       config.ourWorkingDir + "/agh-backup/AdGuardHome",
-		newBinName:       config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome",
+		updateDir:        Context.workDir + "/agh-update-" + newver,
+		backupDir:        Context.workDir + "/agh-backup",
+		configName:       Context.workDir + "/AdGuardHome.yaml",
+		updateConfigName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome.yaml",
+		curBinName:       Context.workDir + "/AdGuardHome",
+		bkpBinName:       Context.workDir + "/agh-backup/AdGuardHome",
+		newBinName:       Context.workDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome",
 	}
 
 	if uu.pkgURL != u.pkgURL ||
diff --git a/home/dns.go b/home/dns.go
index 4d5dceb3..167662b1 100644
--- a/home/dns.go
+++ b/home/dns.go
@@ -25,7 +25,7 @@ func onConfigModified() {
 // Please note that we must do it even if we don't start it
 // so that we had access to the query log and the stats
 func initDNSServer() error {
-	baseDir := config.getDataDir()
+	baseDir := Context.getDataDir()
 
 	err := os.MkdirAll(baseDir, 0755)
 	if err != nil {
@@ -71,8 +71,8 @@ func initDNSServer() error {
 	}
 
 	sessFilename := filepath.Join(baseDir, "sessions.db")
-	config.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60)
-	if config.auth == nil {
+	Context.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60)
+	if Context.auth == nil {
 		closeDNSServer()
 		return fmt.Errorf("Couldn't initialize Auth module")
 	}
@@ -294,9 +294,9 @@ func closeDNSServer() {
 		Context.queryLog = nil
 	}
 
-	if config.auth != nil {
-		config.auth.Close()
-		config.auth = nil
+	if Context.auth != nil {
+		Context.auth.Close()
+		Context.auth = nil
 	}
 
 	log.Debug("Closed all DNS modules")
diff --git a/home/filter.go b/home/filter.go
index 6b0a16ef..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
 		}
@@ -424,7 +425,7 @@ func parseFilterContents(contents []byte) (int, string) {
 func (filter *filter) update() (bool, error) {
 	log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
 
-	resp, err := config.client.Get(filter.URL)
+	resp, err := Context.client.Get(filter.URL)
 	if resp != nil && resp.Body != nil {
 		defer resp.Body.Close()
 	}
@@ -538,7 +539,7 @@ func (filter *filter) unload() {
 
 // Path to the filter contents
 func (filter *filter) Path() string {
-	return filepath.Join(config.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
+	return filepath.Join(Context.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
 }
 
 // LastTimeUpdated returns the time when the filter was last time updated
diff --git a/home/filter_test.go b/home/filter_test.go
index 63736c38..edda556a 100644
--- a/home/filter_test.go
+++ b/home/filter_test.go
@@ -10,7 +10,12 @@ import (
 )
 
 func TestFilters(t *testing.T) {
-	config.client = &http.Client{
+	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 982a10ef..00000000
--- a/home/helpers.go
+++ /dev/null
@@ -1,380 +0,0 @@
-package home
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-	"runtime"
-	"strconv"
-	"strings"
-	"syscall"
-	"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" {
-			config.controlLock.Lock()
-			defer config.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 := config.ourConfigFilename
-	if !filepath.IsAbs(configfile) {
-		configfile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
-	}
-	_, 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 !config.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 config.firstRun &&
-			!(strings.HasPrefix(r.URL.Path, "/install.") ||
-				strings.HasPrefix(r.URL.Path, "/__locales/") ||
-				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}
-}
-
-// ------------------
-// network interfaces
-// ------------------
-type netInterface struct {
-	Name         string   `json:"name"`
-	MTU          int      `json:"mtu"`
-	HardwareAddr string   `json:"hardware_address"`
-	Addresses    []string `json:"ip_addresses"`
-	Flags        string   `json:"flags"`
-}
-
-// 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 := getValidNetInterfaces()
-	if err != nil {
-		return nil, errorx.Decorate(err, "Couldn't get interfaces")
-	}
-	if len(ifaces) == 0 {
-		return nil, errors.New("couldn't find any legible interface")
-	}
-
-	var netInterfaces []netInterface
-
-	for _, iface := range ifaces {
-		addrs, e := iface.Addrs()
-		if e != nil {
-			return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
-		}
-
-		netIface := netInterface{
-			Name:         iface.Name,
-			MTU:          iface.MTU,
-			HardwareAddr: iface.HardwareAddr.String(),
-		}
-
-		if iface.Flags != 0 {
-			netIface.Flags = iface.Flags.String()
-		}
-
-		// we don't want link-local addresses in json, so skip them
-		for _, addr := range addrs {
-			ipnet, ok := addr.(*net.IPNet)
-			if !ok {
-				// not an IPNet, should not happen
-				return nil, fmt.Errorf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
-			}
-			// ignore link-local
-			if ipnet.IP.IsLinkLocalUnicast() {
-				continue
-			}
-			netIface.Addresses = append(netIface.Addresses, ipnet.IP.String())
-		}
-		if len(netIface.Addresses) != 0 {
-			netInterfaces = append(netInterfaces, netIface)
-		}
-	}
-
-	return netInterfaces, nil
-}
-
-// 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 {
-	ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
-	if err != nil {
-		return err
-	}
-	_ = ln.Close()
-
-	// It seems that net.Listener.Close() doesn't close file descriptors right away.
-	// We wait for some time and hope that this fd will be closed.
-	time.Sleep(100 * time.Millisecond)
-	return nil
-}
-
-func checkPacketPortAvailable(host string, port int) error {
-	ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port)))
-	if err != nil {
-		return err
-	}
-	_ = ln.Close()
-
-	// It seems that net.Listener.Close() doesn't close file descriptors right away.
-	// We wait for some time and hope that this fd will be closed.
-	time.Sleep(100 * time.Millisecond)
-	return err
-}
-
-// 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...)
-}
-
-// check if error is "address already in use"
-func errorIsAddrInUse(err error) bool {
-	errOpError, ok := err.(*net.OpError)
-	if !ok {
-		return false
-	}
-
-	errSyscallError, ok := errOpError.Err.(*os.SyscallError)
-	if !ok {
-		return false
-	}
-
-	errErrno, ok := errSyscallError.Err.(syscall.Errno)
-	if !ok {
-		return false
-	}
-
-	if runtime.GOOS == "windows" {
-		const WSAEADDRINUSE = 10048
-		return errErrno == WSAEADDRINUSE
-	}
-
-	return errErrno == syscall.EADDRINUSE
-}
-
-// ---------------------
-// 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 b414d671..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"
@@ -49,6 +53,9 @@ const versionCheckPeriod = time.Hour * 8
 
 // Global context
 type homeContext struct {
+	// Modules
+	// --
+
 	clients     clientsContainer     // per-client-settings module
 	stats       stats.Stats          // statistics module
 	queryLog    querylog.QueryLog    // query log module
@@ -57,8 +64,29 @@ type homeContext struct {
 	whois       *Whois               // WHOIS module
 	dnsFilter   *dnsfilter.Dnsfilter // DNS filtering module
 	dhcpServer  *dhcpd.Server        // DHCP module
+	auth        *Auth                // HTTP authentication module
 	httpServer  *http.Server         // HTTP module
 	httpsServer HTTPSServer          // HTTPS module
+
+	// Runtime properties
+	// --
+
+	configFilename   string // Config filename (can be overridden via the command line arguments)
+	workDir          string // Location of our directory, used to protect against CWD being somewhere else
+	firstRun         bool   // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
+	pidFileName      string // PID file name.  Empty if no PID file was created.
+	disableUpdate    bool   // If set, don't check for updates
+	controlLock      sync.Mutex
+	transport        *http.Transport
+	client           *http.Client
+	appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
+	// runningAsService flag is set to true when options are passed from the service runner
+	runningAsService bool
+}
+
+// getDataDir returns path to the directory where we store databases and filters
+func (c *homeContext) getDataDir() string {
+	return filepath.Join(c.workDir, dataDir)
 }
 
 // Context - a global context object
@@ -81,17 +109,38 @@ func Main(version string, channel string, armVer string) {
 		return
 	}
 
+	Context.appSignalChannel = make(chan os.Signal)
+	signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
+	go func() {
+		<-Context.appSignalChannel
+		cleanup()
+		cleanupAlways()
+		os.Exit(0)
+	}()
+
 	// run the protection
 	run(args)
 }
 
 // run initializes configuration and runs the AdGuard Home
-// run is a blocking method and it won't exit until the service is stopped!
+// run is a blocking method!
 // nolint
 func run(args options) {
 	// config file path can be overridden by command-line arguments:
 	if args.configFilename != "" {
-		config.ourConfigFilename = args.configFilename
+		Context.configFilename = args.configFilename
+	} else {
+		// Default config file name
+		Context.configFilename = "AdGuardHome.yaml"
+	}
+
+	// Init some of the Context fields right away
+	Context.transport = &http.Transport{
+		DialContext: customDialContext,
+	}
+	Context.client = &http.Client{
+		Timeout:   time.Minute * 5,
+		Transport: Context.transport,
 	}
 
 	// configure working dir and config path
@@ -106,31 +155,22 @@ func run(args options) {
 		msg = msg + " v" + ARMVersion
 	}
 	log.Printf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH, ARMVersion)
-	log.Debug("Current working directory is %s", config.ourWorkingDir)
+	log.Debug("Current working directory is %s", Context.workDir)
 	if args.runningAsService {
 		log.Info("AdGuard Home is running as a service")
 	}
-	config.runningAsService = args.runningAsService
-	config.disableUpdate = args.disableUpdate
+	Context.runningAsService = args.runningAsService
+	Context.disableUpdate = args.disableUpdate
 
-	config.firstRun = detectFirstRun()
-	if config.firstRun {
+	Context.firstRun = detectFirstRun()
+	if Context.firstRun {
 		requireAdminRights()
 	}
 
-	config.appSignalChannel = make(chan os.Signal)
-	signal.Notify(config.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
-	go func() {
-		<-config.appSignalChannel
-		cleanup()
-		cleanupAlways()
-		os.Exit(0)
-	}()
-
 	initConfig()
 	initServices()
 
-	if !config.firstRun {
+	if !Context.firstRun {
 		// Do the upgrade if necessary
 		err := upgradeConfig()
 		if err != nil {
@@ -148,7 +188,7 @@ func run(args options) {
 		}
 	}
 
-	config.DHCP.WorkDir = config.ourWorkingDir
+	config.DHCP.WorkDir = Context.workDir
 	config.DHCP.HTTPRegister = httpRegister
 	config.DHCP.ConfigModified = onConfigModified
 	Context.dhcpServer = dhcpd.Create(config.DHCP)
@@ -157,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
@@ -168,7 +208,7 @@ func run(args options) {
 		config.BindPort = args.bindPort
 	}
 
-	if !config.firstRun {
+	if !Context.firstRun {
 		// Save the updated config
 		err := config.write()
 		if err != nil {
@@ -193,7 +233,7 @@ func run(args options) {
 	}
 
 	if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
-		config.pidFileName = args.pidFile
+		Context.pidFileName = args.pidFile
 	}
 
 	// Initialize and run the admin Web interface
@@ -204,7 +244,7 @@ func run(args options) {
 	registerControlHandlers()
 
 	// add handlers for /install paths, we only need them when we're not configured yet
-	if config.firstRun {
+	if Context.firstRun {
 		log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
 		http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
 		registerInstallHandlers()
@@ -291,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
@@ -331,7 +371,7 @@ func writePIDFile(fn string) bool {
 	return true
 }
 
-// initWorkingDir initializes the ourWorkingDir
+// initWorkingDir initializes the workDir
 // if no command-line arguments specified, we use the directory where our binary file is located
 func initWorkingDir(args options) {
 	execPath, err := os.Executable()
@@ -341,9 +381,9 @@ func initWorkingDir(args options) {
 
 	if args.workDir != "" {
 		// If there is a custom config file, use it's directory as our working dir
-		config.ourWorkingDir = args.workDir
+		Context.workDir = args.workDir
 	} else {
-		config.ourWorkingDir = filepath.Dir(execPath)
+		Context.workDir = filepath.Dir(execPath)
 	}
 }
 
@@ -376,12 +416,12 @@ 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)
 		}
 	} else {
-		logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile)
+		logFilePath := filepath.Join(Context.workDir, ls.LogFile)
 		if filepath.IsAbs(ls.LogFile) {
 			logFilePath = ls.LogFile
 		}
@@ -420,8 +460,8 @@ func stopHTTPServer() {
 
 // This function is called before application exits
 func cleanupAlways() {
-	if len(config.pidFileName) != 0 {
-		_ = os.Remove(config.pidFileName)
+	if len(Context.pidFileName) != 0 {
+		_ = os.Remove(Context.pidFileName)
 	}
 	log.Info("Stopped")
 }
@@ -544,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))
@@ -561,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/home_test.go b/home/home_test.go
index 771c74d0..fba9b43d 100644
--- a/home/home_test.go
+++ b/home/home_test.go
@@ -107,6 +107,9 @@ schema_version: 5
 // . Wait until the filters are downloaded
 // . Stop and cleanup
 func TestHome(t *testing.T) {
+	// Reinit context
+	Context = homeContext{}
+
 	dir := prepareTestDir()
 	defer func() { _ = os.RemoveAll(dir) }()
 	fn := filepath.Join(dir, "AdGuardHome.yaml")
@@ -123,12 +126,12 @@ func TestHome(t *testing.T) {
 	var err error
 	var resp *http.Response
 	h := http.Client{}
-	for i := 0; i != 5; i++ {
+	for i := 0; i != 50; i++ {
 		resp, err = h.Get("http://127.0.0.1:3000/")
 		if err == nil && resp.StatusCode != 404 {
 			break
 		}
-		time.Sleep(1 * time.Second)
+		time.Sleep(100 * time.Millisecond)
 	}
 	assert.Truef(t, err == nil, "%s", err)
 	assert.Equal(t, 200, resp.StatusCode)
@@ -140,7 +143,7 @@ func TestHome(t *testing.T) {
 	// test DNS over UDP
 	r := upstream.NewResolver("127.0.0.1:5354", 3*time.Second)
 	addrs, err := r.LookupIPAddr(context.TODO(), "static.adguard.com")
-	assert.Truef(t, err == nil, "%s", err)
+	assert.Nil(t, err)
 	haveIP := len(addrs) != 0
 	assert.True(t, haveIP)
 
diff --git a/home/service.go b/home/service.go
index edca9244..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"
 )
@@ -34,10 +35,10 @@ func (p *program) Start(s service.Service) error {
 // Stop stops the program
 func (p *program) Stop(s service.Service) error {
 	// Stop should not block. Return with a few seconds.
-	if config.appSignalChannel == nil {
+	if Context.appSignalChannel == nil {
 		os.Exit(0)
 	}
-	config.appSignalChannel <- syscall.SIGINT
+	Context.appSignalChannel <- syscall.SIGINT
 	return nil
 }
 
@@ -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 3a703ebc..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,9 +116,9 @@ 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(config.ourWorkingDir, "dnsfilter.txt")
+	dnsFilterPath := filepath.Join(Context.workDir, "dnsfilter.txt")
 	if _, err := os.Stat(dnsFilterPath); !os.IsNotExist(err) {
 		log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
 		err = os.Remove(dnsFilterPath)
@@ -135,9 +137,9 @@ 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(config.ourWorkingDir, "Corefile")
+	coreFilePath := filepath.Join(Context.workDir, "Corefile")
 	if _, err := os.Stat(coreFilePath); !os.IsNotExist(err) {
 		log.Printf("Deleting %s as we don't need it anymore", coreFilePath)
 		err = os.Remove(coreFilePath)
@@ -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/openapi/openapi.yaml b/openapi/openapi.yaml
index 9ad2c5d1..5f3539f0 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1831,6 +1831,9 @@ definitions:
                 $ref: "#/definitions/CheckConfigRequestInfo"
             web:
                 $ref: "#/definitions/CheckConfigRequestInfo"
+            set_static_ip:
+                type: "boolean"
+                example: false
     CheckConfigRequestInfo:
         type: "object"
         properties:
@@ -1851,6 +1854,8 @@ definitions:
                 $ref: "#/definitions/CheckConfigResponseInfo"
             web:
                 $ref: "#/definitions/CheckConfigResponseInfo"
+            static_ip:
+                $ref: "#/definitions/CheckConfigStaticIpInfo"
     CheckConfigResponseInfo:
         type: "object"
         properties:
@@ -1860,6 +1865,23 @@ definitions:
             can_autofix:
                 type: "boolean"
                 example: false
+    CheckConfigStaticIpInfo:
+        type: "object"
+        properties:
+            static:
+                type: "string"
+                example: "no"
+                description: "Can be: yes, no, error"
+            ip:
+                type: "string"
+                example: "192.168.1.1"
+                description: "Current dynamic IP address. Set if static=no"
+            error:
+                type: "string"
+                example: ""
+                description: "Error text. Set if static=error"
+
+
     InitialConfiguration:
         type: "object"
         description: "AdGuard Home initial configuration (for the first-install wizard)"
diff --git a/util/helpers.go b/util/helpers.go
new file mode 100644
index 00000000..c50c940d
--- /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: %v: %s", command, err, string(out))
+	}
+
+	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/util/network_utils.go b/util/network_utils.go
new file mode 100644
index 00000000..af410201
--- /dev/null
+++ b/util/network_utils.go
@@ -0,0 +1,194 @@
+package util
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"os"
+	"runtime"
+	"strconv"
+	"syscall"
+	"time"
+
+	"github.com/AdguardTeam/golibs/log"
+
+	"github.com/joomcode/errorx"
+)
+
+// 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 := GetValidNetInterfaces()
+	if err != nil {
+		return nil, errorx.Decorate(err, "Couldn't get interfaces")
+	}
+	if len(ifaces) == 0 {
+		return nil, errors.New("couldn't find any legible interface")
+	}
+
+	var netInterfaces []NetInterface
+
+	for _, iface := range ifaces {
+		addrs, e := iface.Addrs()
+		if e != nil {
+			return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
+		}
+
+		netIface := NetInterface{
+			Name:         iface.Name,
+			MTU:          iface.MTU,
+			HardwareAddr: iface.HardwareAddr.String(),
+		}
+
+		if iface.Flags != 0 {
+			netIface.Flags = iface.Flags.String()
+		}
+
+		// Collect network interface addresses
+		for _, addr := range addrs {
+			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() {
+				continue
+			}
+			// 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)
+		}
+	}
+
+	return netInterfaces, nil
+}
+
+// Get interface name by its IP address.
+func GetInterfaceByIP(ip string) string {
+	ifaces, err := GetValidNetInterfacesForWeb()
+	if err != nil {
+		return ""
+	}
+
+	for _, iface := range ifaces {
+		for _, addr := range iface.Addresses {
+			if ip == addr {
+				return iface.Name
+			}
+		}
+	}
+
+	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 {
+	ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
+	if err != nil {
+		return err
+	}
+	_ = ln.Close()
+
+	// It seems that net.Listener.Close() doesn't close file descriptors right away.
+	// We wait for some time and hope that this fd will be closed.
+	time.Sleep(100 * time.Millisecond)
+	return nil
+}
+
+func CheckPacketPortAvailable(host string, port int) error {
+	ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port)))
+	if err != nil {
+		return err
+	}
+	_ = ln.Close()
+
+	// It seems that net.Listener.Close() doesn't close file descriptors right away.
+	// We wait for some time and hope that this fd will be closed.
+	time.Sleep(100 * time.Millisecond)
+	return err
+}
+
+// check if error is "address already in use"
+func ErrorIsAddrInUse(err error) bool {
+	errOpError, ok := err.(*net.OpError)
+	if !ok {
+		return false
+	}
+
+	errSyscallError, ok := errOpError.Err.(*os.SyscallError)
+	if !ok {
+		return false
+	}
+
+	errErrno, ok := errSyscallError.Err.(syscall.Errno)
+	if !ok {
+		return false
+	}
+
+	if runtime.GOOS == "windows" {
+		const WSAEADDRINUSE = 10048
+		return errErrno == WSAEADDRINUSE
+	}
+
+	return errErrno == syscall.EADDRINUSE
+}
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