diff --git a/control.go b/control.go
index 3dca2c36..35214d82 100644
--- a/control.go
+++ b/control.go
@@ -2,14 +2,12 @@ package main
 
 import (
 	"bytes"
-	"context"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net"
 	"net/http"
 	"os"
-	"os/exec"
 	"sort"
 	"strconv"
 	"strings"
@@ -971,263 +969,6 @@ func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-type firstRunData struct {
-	WebPort    int                    `json:"web_port"`
-	DNSPort    int                    `json:"dns_port"`
-	Interfaces map[string]interface{} `json:"interfaces"`
-}
-
-// Get initial installation settings
-func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
-	log.Tracef("%s %v", r.Method, r.URL)
-	data := firstRunData{}
-	data.WebPort = 80
-	data.DNSPort = 53
-
-	ifaces, err := getValidNetInterfacesForWeb()
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
-		return
-	}
-
-	data.Interfaces = make(map[string]interface{})
-	for _, iface := range ifaces {
-		data.Interfaces[iface.Name] = iface
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	err = json.NewEncoder(w).Encode(data)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
-		return
-	}
-}
-
-type checkConfigReqEnt struct {
-	Port    int    `json:"port"`
-	IP      string `json:"ip"`
-	Autofix bool   `json:"autofix"`
-}
-type checkConfigReq struct {
-	Web checkConfigReqEnt `json:"web"`
-	DNS checkConfigReqEnt `json:"dns"`
-}
-
-type checkConfigRespEnt struct {
-	Status     string `json:"status"`
-	CanAutofix bool   `json:"can_autofix"`
-}
-type checkConfigResp struct {
-	Web checkConfigRespEnt `json:"web"`
-	DNS checkConfigRespEnt `json:"dns"`
-}
-
-// Check if ports are available, respond with results
-func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
-	log.Tracef("%s %v", r.Method, r.URL)
-	reqData := checkConfigReq{}
-	respData := checkConfigResp{}
-	err := json.NewDecoder(r.Body).Decode(&reqData)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err)
-		return
-	}
-
-	if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
-		err = 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)
-
-		if errorIsAddrInUse(err) {
-			canAutofix := checkDNSStubListener()
-			if canAutofix && reqData.DNS.Autofix {
-
-				err = disableDNSStubListener()
-				if err != nil {
-					log.Error("Couldn't disable DNSStubListener: %s", err)
-				}
-
-				err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
-				canAutofix = false
-			}
-
-			respData.DNS.CanAutofix = canAutofix
-		}
-
-		if err == nil {
-			err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
-		}
-
-		if err != nil {
-			respData.DNS.Status = fmt.Sprintf("%v", err)
-		}
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	err = json.NewEncoder(w).Encode(respData)
-	if err != nil {
-		httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err)
-		return
-	}
-}
-
-// Check if DNSStubListener is active
-func checkDNSStubListener() bool {
-	cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved")
-	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",
-			cmd.Path, err, cmd.ProcessState.ExitCode())
-		return false
-	}
-
-	cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf")
-	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",
-			cmd.Path, err, cmd.ProcessState.ExitCode())
-		return false
-	}
-
-	return true
-}
-
-// Deactivate DNSStubListener
-func disableDNSStubListener() error {
-	cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf")
-	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
-	_, err := cmd.Output()
-	if err != nil {
-		return err
-	}
-	if cmd.ProcessState.ExitCode() != 0 {
-		return fmt.Errorf("process %s exited with an error: %d",
-			cmd.Path, cmd.ProcessState.ExitCode())
-	}
-
-	cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved")
-	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
-	_, err = cmd.Output()
-	if err != nil {
-		return err
-	}
-	if cmd.ProcessState.ExitCode() != 0 {
-		return fmt.Errorf("process %s exited with an error: %d",
-			cmd.Path, cmd.ProcessState.ExitCode())
-	}
-
-	return nil
-}
-
-type applyConfigReqEnt struct {
-	IP   string `json:"ip"`
-	Port int    `json:"port"`
-}
-type applyConfigReq struct {
-	Web      applyConfigReqEnt `json:"web"`
-	DNS      applyConfigReqEnt `json:"dns"`
-	Username string            `json:"username"`
-	Password string            `json:"password"`
-}
-
-// Copy installation parameters between two configuration objects
-func copyInstallSettings(dst *configuration, src *configuration) {
-	dst.BindHost = src.BindHost
-	dst.BindPort = src.BindPort
-	dst.DNS.BindHost = src.DNS.BindHost
-	dst.DNS.Port = src.DNS.Port
-	dst.AuthName = src.AuthName
-	dst.AuthPass = src.AuthPass
-}
-
-// Apply new configuration, start DNS server, restart Web server
-func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
-	log.Tracef("%s %v", r.Method, r.URL)
-	newSettings := applyConfigReq{}
-	err := json.NewDecoder(r.Body).Decode(&newSettings)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
-		return
-	}
-
-	if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 {
-		httpError(w, http.StatusBadRequest, "port value can't be 0")
-		return
-	}
-
-	restartHTTP := true
-	if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
-		// no need to rebind
-		restartHTTP = false
-	}
-
-	// validate that hosts and ports are bindable
-	if restartHTTP {
-		err = 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)
-			return
-		}
-	}
-
-	err = 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)
-	if err != nil {
-		httpError(w, http.StatusBadRequest, "%s", err)
-		return
-	}
-
-	var curConfig configuration
-	copyInstallSettings(&curConfig, &config)
-
-	config.firstRun = false
-	config.BindHost = newSettings.Web.IP
-	config.BindPort = newSettings.Web.Port
-	config.DNS.BindHost = newSettings.DNS.IP
-	config.DNS.Port = newSettings.DNS.Port
-	config.AuthName = newSettings.Username
-	config.AuthPass = newSettings.Password
-
-	err = startDNSServer()
-	if err != nil {
-		config.firstRun = true
-		copyInstallSettings(&config, &curConfig)
-		httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
-		return
-	}
-
-	err = config.write()
-	if err != nil {
-		config.firstRun = true
-		copyInstallSettings(&config, &curConfig)
-		httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
-		return
-	}
-
-	// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
-	// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
-	if restartHTTP {
-		go func() {
-			httpServer.Shutdown(context.TODO())
-		}()
-	}
-
-	returnOK(w)
-}
-
 // --------------
 // DNS-over-HTTPS
 // --------------
@@ -1249,12 +990,6 @@ func handleDOH(w http.ResponseWriter, r *http.Request) {
 // ------------------------
 // registration of handlers
 // ------------------------
-func registerInstallHandlers() {
-	http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
-	http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
-	http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
-}
-
 func registerControlHandlers() {
 	http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus))))
 	http.HandleFunc("/control/enable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionEnable))))
diff --git a/control_install.go b/control_install.go
new file mode 100644
index 00000000..c4338e9d
--- /dev/null
+++ b/control_install.go
@@ -0,0 +1,276 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"os/exec"
+	"strconv"
+
+	"github.com/AdguardTeam/golibs/log"
+)
+
+type firstRunData struct {
+	WebPort    int                    `json:"web_port"`
+	DNSPort    int                    `json:"dns_port"`
+	Interfaces map[string]interface{} `json:"interfaces"`
+}
+
+// Get initial installation settings
+func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+	data := firstRunData{}
+	data.WebPort = 80
+	data.DNSPort = 53
+
+	ifaces, err := getValidNetInterfacesForWeb()
+	if err != nil {
+		httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
+		return
+	}
+
+	data.Interfaces = make(map[string]interface{})
+	for _, iface := range ifaces {
+		data.Interfaces[iface.Name] = iface
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	err = json.NewEncoder(w).Encode(data)
+	if err != nil {
+		httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
+		return
+	}
+}
+
+type checkConfigReqEnt struct {
+	Port    int    `json:"port"`
+	IP      string `json:"ip"`
+	Autofix bool   `json:"autofix"`
+}
+type checkConfigReq struct {
+	Web checkConfigReqEnt `json:"web"`
+	DNS checkConfigReqEnt `json:"dns"`
+}
+
+type checkConfigRespEnt struct {
+	Status     string `json:"status"`
+	CanAutofix bool   `json:"can_autofix"`
+}
+type checkConfigResp struct {
+	Web checkConfigRespEnt `json:"web"`
+	DNS checkConfigRespEnt `json:"dns"`
+}
+
+// Check if ports are available, respond with results
+func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+	reqData := checkConfigReq{}
+	respData := checkConfigResp{}
+	err := json.NewDecoder(r.Body).Decode(&reqData)
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err)
+		return
+	}
+
+	if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
+		err = 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)
+
+		if errorIsAddrInUse(err) {
+			canAutofix := checkDNSStubListener()
+			if canAutofix && reqData.DNS.Autofix {
+
+				err = disableDNSStubListener()
+				if err != nil {
+					log.Error("Couldn't disable DNSStubListener: %s", err)
+				}
+
+				err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
+				canAutofix = false
+			}
+
+			respData.DNS.CanAutofix = canAutofix
+		}
+
+		if err == nil {
+			err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
+		}
+
+		if err != nil {
+			respData.DNS.Status = fmt.Sprintf("%v", err)
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	err = json.NewEncoder(w).Encode(respData)
+	if err != nil {
+		httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err)
+		return
+	}
+}
+
+// Check if DNSStubListener is active
+func checkDNSStubListener() bool {
+	cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved")
+	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",
+			cmd.Path, err, cmd.ProcessState.ExitCode())
+		return false
+	}
+
+	cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf")
+	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",
+			cmd.Path, err, cmd.ProcessState.ExitCode())
+		return false
+	}
+
+	return true
+}
+
+// Deactivate DNSStubListener
+func disableDNSStubListener() error {
+	cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf")
+	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
+	_, err := cmd.Output()
+	if err != nil {
+		return err
+	}
+	if cmd.ProcessState.ExitCode() != 0 {
+		return fmt.Errorf("process %s exited with an error: %d",
+			cmd.Path, cmd.ProcessState.ExitCode())
+	}
+
+	cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved")
+	log.Tracef("executing %s %v", cmd.Path, cmd.Args)
+	_, err = cmd.Output()
+	if err != nil {
+		return err
+	}
+	if cmd.ProcessState.ExitCode() != 0 {
+		return fmt.Errorf("process %s exited with an error: %d",
+			cmd.Path, cmd.ProcessState.ExitCode())
+	}
+
+	return nil
+}
+
+type applyConfigReqEnt struct {
+	IP   string `json:"ip"`
+	Port int    `json:"port"`
+}
+type applyConfigReq struct {
+	Web      applyConfigReqEnt `json:"web"`
+	DNS      applyConfigReqEnt `json:"dns"`
+	Username string            `json:"username"`
+	Password string            `json:"password"`
+}
+
+// Copy installation parameters between two configuration objects
+func copyInstallSettings(dst *configuration, src *configuration) {
+	dst.BindHost = src.BindHost
+	dst.BindPort = src.BindPort
+	dst.DNS.BindHost = src.DNS.BindHost
+	dst.DNS.Port = src.DNS.Port
+	dst.AuthName = src.AuthName
+	dst.AuthPass = src.AuthPass
+}
+
+// Apply new configuration, start DNS server, restart Web server
+func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
+	log.Tracef("%s %v", r.Method, r.URL)
+	newSettings := applyConfigReq{}
+	err := json.NewDecoder(r.Body).Decode(&newSettings)
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
+		return
+	}
+
+	if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 {
+		httpError(w, http.StatusBadRequest, "port value can't be 0")
+		return
+	}
+
+	restartHTTP := true
+	if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
+		// no need to rebind
+		restartHTTP = false
+	}
+
+	// validate that hosts and ports are bindable
+	if restartHTTP {
+		err = 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)
+			return
+		}
+	}
+
+	err = 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)
+	if err != nil {
+		httpError(w, http.StatusBadRequest, "%s", err)
+		return
+	}
+
+	var curConfig configuration
+	copyInstallSettings(&curConfig, &config)
+
+	config.firstRun = false
+	config.BindHost = newSettings.Web.IP
+	config.BindPort = newSettings.Web.Port
+	config.DNS.BindHost = newSettings.DNS.IP
+	config.DNS.Port = newSettings.DNS.Port
+	config.AuthName = newSettings.Username
+	config.AuthPass = newSettings.Password
+
+	err = startDNSServer()
+	if err != nil {
+		config.firstRun = true
+		copyInstallSettings(&config, &curConfig)
+		httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
+		return
+	}
+
+	err = config.write()
+	if err != nil {
+		config.firstRun = true
+		copyInstallSettings(&config, &curConfig)
+		httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
+		return
+	}
+
+	// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
+	// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
+	if restartHTTP {
+		go func() {
+			httpServer.Shutdown(context.TODO())
+		}()
+	}
+
+	returnOK(w)
+}
+
+func registerInstallHandlers() {
+	http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
+	http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
+	http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
+}