diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index f8eecedb..aa95a923 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -32,7 +32,11 @@
"dhcp_ip_addresses": "IP addresses",
"dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expires",
- "dhcp_warning": "If you want to enable the built-in DHCP server, make sure that there is no other active DHCP server. Otherwise, it can break the internet for connected devices!",
+ "dhcp_warning": "If you want to enable DHCP server anyway, make sure that there is no other active DHCP server in your network. Otherwise, it can break the Internet for connected devices!",
+ "dhcp_error": "We could not determine whether there is another DHCP server in the network.",
+ "dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
+ "dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}0>. We will automatically set this IP address as static if you press Enable DHCP button.",
+ "error_details": "Error details",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js
index 5335586b..437f9265 100644
--- a/client/src/components/Settings/Dhcp/index.js
+++ b/client/src/components/Settings/Dhcp/index.js
@@ -3,10 +3,12 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
+import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
import Form from './Form';
import Leases from './Leases';
import Interface from './Interface';
import Card from '../../ui/Card';
+import Accordion from '../../ui/Accordion';
class Dhcp extends Component {
handleFormSubmit = (values) => {
@@ -19,11 +21,12 @@ class Dhcp extends Component {
getToggleDhcpButton = () => {
const {
- config, active, processingDhcp, processingConfig,
+ config, check, processingDhcp, processingConfig,
} = this.props.dhcp;
- const activeDhcpFound = active && active.found;
+ const otherDhcpFound =
+ check && check.otherServer && check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
const filledConfig = Object.keys(config).every((key) => {
- if (key === 'enabled') {
+ if (key === 'enabled' || key === 'icmp_timeout_msec') {
return true;
}
@@ -50,7 +53,8 @@ class Dhcp extends Component {
onClick={() => this.handleToggle(config)}
disabled={
!filledConfig
- || activeDhcpFound
+ || !check
+ || otherDhcpFound
|| processingDhcp
|| processingConfig
}
@@ -60,33 +64,89 @@ class Dhcp extends Component {
);
}
- getActiveDhcpMessage = () => {
- const { active } = this.props.dhcp;
-
- if (active) {
- if (active.error) {
- return (
-
- {active.error}
-
- );
- }
+ getActiveDhcpMessage = (t, check) => {
+ const { found } = check.otherServer;
+ if (found === DHCP_STATUS_RESPONSE.ERROR) {
return (
-
- {active.found ? (
-
- dhcp_found
-
- ) : (
-
- dhcp_not_found
-
- )}
+
+
dhcp_error
+
+
+ {check.otherServer.error}
+
+
);
}
+ return (
+
+ {found === DHCP_STATUS_RESPONSE.YES ? (
+
+ dhcp_found
+
+ ) : (
+
+ dhcp_not_found
+
+ )}
+
+ );
+ }
+
+ getDhcpWarning = (check) => {
+ if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
+ return '';
+ }
+
+ return (
+
+ dhcp_warning
+
+ );
+ }
+
+ getStaticIpWarning = (t, check, interfaceName) => {
+ if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
+ return (
+
+
+
dhcp_static_ip_error
+
+
+ {check.staticIP.error}
+
+
+
+
+
+ );
+ } else if (
+ check.staticIP.static === DHCP_STATUS_RESPONSE.NO
+ && check.staticIP.ip
+ && interfaceName
+ ) {
+ return (
+
+
+ example,
+ ]}
+ values={{
+ interfaceName,
+ ipAddress: check.staticIP.ip,
+ }}
+ >
+ dhcp_dynamic_ip_found
+
+
+
+
+ );
+ }
+
return '';
}
@@ -131,17 +191,21 @@ class Dhcp extends Component {
this.props.findActiveDhcp(dhcp.config.interface_name)
}
disabled={
- !dhcp.config.interface_name
+ dhcp.config.enabled
+ || !dhcp.config.interface_name
|| dhcp.processingConfig
}
>
check_dhcp_servers
- {this.getActiveDhcpMessage()}
-
- dhcp_warning
-
+ {!enabled && dhcp.check &&
+
+ {this.getStaticIpWarning(t, dhcp.check, interface_name)}
+ {this.getActiveDhcpMessage(t, dhcp.check)}
+ {this.getDhcpWarning(dhcp.check)}
+
+ }
}
diff --git a/client/src/components/ui/Accordion.css b/client/src/components/ui/Accordion.css
new file mode 100644
index 00000000..d62695aa
--- /dev/null
+++ b/client/src/components/ui/Accordion.css
@@ -0,0 +1,32 @@
+.accordion {
+ color: #495057;
+}
+
+.accordion__label {
+ position: relative;
+ display: inline-block;
+ padding-left: 25px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.accordion__label:after {
+ content: "";
+ position: absolute;
+ top: 7px;
+ left: 0;
+ width: 17px;
+ height: 10px;
+ background-image: url("./svg/chevron-down.svg");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100%;
+}
+
+.accordion__label--open:after {
+ transform: rotate(180deg);
+}
+
+.accordion__content {
+ padding-top: 5px;
+}
diff --git a/client/src/components/ui/Accordion.js b/client/src/components/ui/Accordion.js
new file mode 100644
index 00000000..90fa25f9
--- /dev/null
+++ b/client/src/components/ui/Accordion.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+import './Accordion.css';
+
+class Accordion extends Component {
+ state = {
+ isOpen: false,
+ }
+
+ handleClick = () => {
+ this.setState(prevState => ({ isOpen: !prevState.isOpen }));
+ };
+
+ render() {
+ const accordionClass = this.state.isOpen
+ ? 'accordion__label accordion__label--open'
+ : 'accordion__label';
+
+ return (
+
+
+ {this.props.label}
+
+ {this.state.isOpen && (
+
+ {this.props.children}
+
+ )}
+
+ );
+ }
+}
+
+Accordion.propTypes = {
+ children: PropTypes.node.isRequired,
+ label: PropTypes.string.isRequired,
+};
+
+export default Accordion;
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 79deabdb..703a8dff 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -157,3 +157,9 @@ export const UNSAFE_PORTS = [
];
export const ALL_INTERFACES_IP = '0.0.0.0';
+
+export const DHCP_STATUS_RESPONSE = {
+ YES: 'yes',
+ NO: 'no',
+ ERROR: 'error',
+};
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 404679eb..25156688 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -292,18 +292,31 @@ const dhcp = handleActions({
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
- [actions.findActiveDhcpSuccess]: (state, { payload }) => ({
- ...state,
- active: payload,
- processingStatus: false,
- }),
+ [actions.findActiveDhcpSuccess]: (state, { payload }) => {
+ const {
+ other_server: otherServer,
+ static_ip: staticIP,
+ } = payload;
+
+ const newState = {
+ ...state,
+ check: {
+ otherServer,
+ staticIP,
+ },
+ processingStatus: false,
+ };
+ return newState;
+ },
[actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }),
[actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }),
[actions.toggleDhcpSuccess]: (state) => {
const { config } = state;
const newConfig = { ...config, enabled: !config.enabled };
- const newState = { ...state, config: newConfig, processingDhcp: false };
+ const newState = {
+ ...state, config: newConfig, check: null, processingDhcp: false,
+ };
return newState;
},
@@ -324,7 +337,7 @@ const dhcp = handleActions({
config: {
enabled: false,
},
- active: null,
+ check: null,
leases: [],
});
diff --git a/config.go b/config.go
index 07466925..4e63bfb6 100644
--- a/config.go
+++ b/config.go
@@ -134,6 +134,10 @@ var config = configuration{
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
+ DHCP: dhcpd.ServerConfig{
+ LeaseDuration: 86400,
+ ICMPTimeout: 1000,
+ },
SchemaVersion: currentSchemaVersion,
}
diff --git a/dhcp.go b/dhcp.go
index ac9c36c5..4bd0c463 100644
--- a/dhcp.go
+++ b/dhcp.go
@@ -2,14 +2,18 @@ package main
import (
"encoding/json"
+ "errors"
"fmt"
"io/ioutil"
"net"
"net/http"
+ "os/exec"
+ "runtime"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
+ "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
@@ -58,7 +62,17 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
}
if newconfig.Enabled {
- err := dhcpServer.Start(&newconfig)
+
+ staticIP, err := hasStaticIP(newconfig.InterfaceName)
+ if !staticIP && err == nil {
+ err = setStaticIP(newconfig.InterfaceName)
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
+ return
+ }
+ }
+
+ err = dhcpServer.Start(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
@@ -130,6 +144,10 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
}
}
+// Perform the following tasks:
+// . Search for another DHCP server running
+// . Check if a static IP is configured for the network interface
+// Respond with results
func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body)
@@ -147,13 +165,35 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
http.Error(w, errorText, http.StatusBadRequest)
return
}
+
found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName)
- result := map[string]interface{}{}
- if err != nil {
- result["error"] = err.Error()
- } else {
- result["found"] = found
+
+ othSrv := map[string]interface{}{}
+ foundVal := "no"
+ if found {
+ foundVal = "yes"
+ } else if err != nil {
+ foundVal = "error"
+ othSrv["error"] = err.Error()
}
+ othSrv["found"] = foundVal
+
+ staticIP := map[string]interface{}{}
+ 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["static"] = staticIPStatus
+
+ result := map[string]interface{}{}
+ result["other_server"] = othSrv
+ result["static_ip"] = staticIP
+
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
@@ -162,6 +202,137 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
}
}
+// 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 startDHCPServer() error {
if !config.DHCP.Enabled {
// not enabled, don't do anything