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}}. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}. 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