Merge: DHCP: check/set static IP

Close #686

* commit '828d3121be807daa8f839dfa7a7ac4ba8a6e7cd8':
  * client: show message if there is no static ip
  * client: rename constant
  * hasStaticIP: use properly named boolean variable
  + client: static_ip warnings
  * client: error text
  * client: disable DHCP check if server enabled and hide errors on disable
  * client: hide error if DHCP enabled and require check DHCP before enabling
  * client: accordion styles
  - client: fix DHCP fields validation
  * client: fix DHCP error message
  + config: set default parameters for DHCP server
  + /control/dhcp/set_config: set static IP
  + /control/dhcp/find_active_dhcp: detect static IP on Linux
  * /control/dhcp/find_active_dhcp: new JSON response format
This commit is contained in:
Simon Zolin 2019-04-15 12:59:16 +03:00
commit 08bedacf0a
8 changed files with 381 additions and 44 deletions

View File

@ -32,7 +32,11 @@
"dhcp_ip_addresses": "IP addresses", "dhcp_ip_addresses": "IP addresses",
"dhcp_table_hostname": "Hostname", "dhcp_table_hostname": "Hostname",
"dhcp_table_expires": "Expires", "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", "back": "Back",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"settings": "Settings", "settings": "Settings",

View File

@ -3,10 +3,12 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
import Form from './Form'; import Form from './Form';
import Leases from './Leases'; import Leases from './Leases';
import Interface from './Interface'; import Interface from './Interface';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Accordion from '../../ui/Accordion';
class Dhcp extends Component { class Dhcp extends Component {
handleFormSubmit = (values) => { handleFormSubmit = (values) => {
@ -19,11 +21,12 @@ class Dhcp extends Component {
getToggleDhcpButton = () => { getToggleDhcpButton = () => {
const { const {
config, active, processingDhcp, processingConfig, config, check, processingDhcp, processingConfig,
} = this.props.dhcp; } = 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) => { const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled') { if (key === 'enabled' || key === 'icmp_timeout_msec') {
return true; return true;
} }
@ -50,7 +53,8 @@ class Dhcp extends Component {
onClick={() => this.handleToggle(config)} onClick={() => this.handleToggle(config)}
disabled={ disabled={
!filledConfig !filledConfig
|| activeDhcpFound || !check
|| otherDhcpFound
|| processingDhcp || processingDhcp
|| processingConfig || processingConfig
} }
@ -60,33 +64,89 @@ class Dhcp extends Component {
); );
} }
getActiveDhcpMessage = () => { getActiveDhcpMessage = (t, check) => {
const { active } = this.props.dhcp; const { found } = check.otherServer;
if (active) {
if (active.error) {
return (
<div className="text-danger mb-2">
{active.error}
</div>
);
}
if (found === DHCP_STATUS_RESPONSE.ERROR) {
return ( return (
<div className="mb-2"> <div className="text-danger mb-2">
{active.found ? ( <Trans>dhcp_error</Trans>
<div className="text-danger"> <div className="mt-2 mb-2">
<Trans>dhcp_found</Trans> <Accordion label={t('error_details')}>
</div> <span>{check.otherServer.error}</span>
) : ( </Accordion>
<div className="text-secondary"> </div>
<Trans>dhcp_not_found</Trans>
</div>
)}
</div> </div>
); );
} }
return (
<div className="mb-2">
{found === DHCP_STATUS_RESPONSE.YES ? (
<div className="text-danger">
<Trans>dhcp_found</Trans>
</div>
) : (
<div className="text-secondary">
<Trans>dhcp_not_found</Trans>
</div>
)}
</div>
);
}
getDhcpWarning = (check) => {
if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
return '';
}
return (
<div className="text-danger">
<Trans>dhcp_warning</Trans>
</div>
);
}
getStaticIpWarning = (t, check, interfaceName) => {
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
return (
<Fragment>
<div className="text-danger mb-2">
<Trans>dhcp_static_ip_error</Trans>
<div className="mt-2 mb-2">
<Accordion label={t('error_details')}>
<span>{check.staticIP.error}</span>
</Accordion>
</div>
</div>
<hr className="mt-4 mb-4"/>
</Fragment>
);
} else if (
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
&& check.staticIP.ip
&& interfaceName
) {
return (
<Fragment>
<div className="text-secondary mb-2">
<Trans
components={[
<strong key="0">example</strong>,
]}
values={{
interfaceName,
ipAddress: check.staticIP.ip,
}}
>
dhcp_dynamic_ip_found
</Trans>
</div>
<hr className="mt-4 mb-4"/>
</Fragment>
);
}
return ''; return '';
} }
@ -131,17 +191,21 @@ class Dhcp extends Component {
this.props.findActiveDhcp(dhcp.config.interface_name) this.props.findActiveDhcp(dhcp.config.interface_name)
} }
disabled={ disabled={
!dhcp.config.interface_name dhcp.config.enabled
|| !dhcp.config.interface_name
|| dhcp.processingConfig || dhcp.processingConfig
} }
> >
<Trans>check_dhcp_servers</Trans> <Trans>check_dhcp_servers</Trans>
</button> </button>
</div> </div>
{this.getActiveDhcpMessage()} {!enabled && dhcp.check &&
<div className="text-danger"> <Fragment>
<Trans>dhcp_warning</Trans> {this.getStaticIpWarning(t, dhcp.check, interface_name)}
</div> {this.getActiveDhcpMessage(t, dhcp.check)}
{this.getDhcpWarning(dhcp.check)}
</Fragment>
}
</Fragment> </Fragment>
} }
</div> </div>

View File

@ -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;
}

View File

@ -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 (
<div className="accordion">
<div
className={accordionClass}
onClick={this.handleClick}
>
{this.props.label}
</div>
{this.state.isOpen && (
<div className="accordion__content">
{this.props.children}
</div>
)}
</div>
);
}
}
Accordion.propTypes = {
children: PropTypes.node.isRequired,
label: PropTypes.string.isRequired,
};
export default Accordion;

View File

@ -157,3 +157,9 @@ export const UNSAFE_PORTS = [
]; ];
export const ALL_INTERFACES_IP = '0.0.0.0'; export const ALL_INTERFACES_IP = '0.0.0.0';
export const DHCP_STATUS_RESPONSE = {
YES: 'yes',
NO: 'no',
ERROR: 'error',
};

View File

@ -292,18 +292,31 @@ const dhcp = handleActions({
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }), [actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }), [actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
[actions.findActiveDhcpSuccess]: (state, { payload }) => ({ [actions.findActiveDhcpSuccess]: (state, { payload }) => {
...state, const {
active: payload, other_server: otherServer,
processingStatus: false, static_ip: staticIP,
}), } = payload;
const newState = {
...state,
check: {
otherServer,
staticIP,
},
processingStatus: false,
};
return newState;
},
[actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }), [actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }),
[actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }), [actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }),
[actions.toggleDhcpSuccess]: (state) => { [actions.toggleDhcpSuccess]: (state) => {
const { config } = state; const { config } = state;
const newConfig = { ...config, enabled: !config.enabled }; const newConfig = { ...config, enabled: !config.enabled };
const newState = { ...state, config: newConfig, processingDhcp: false }; const newState = {
...state, config: newConfig, check: null, processingDhcp: false,
};
return newState; return newState;
}, },
@ -324,7 +337,7 @@ const dhcp = handleActions({
config: { config: {
enabled: false, enabled: false,
}, },
active: null, check: null,
leases: [], leases: [],
}); });

View File

@ -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: 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"}, {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, SchemaVersion: currentSchemaVersion,
} }

183
dhcp.go
View File

@ -2,14 +2,18 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os/exec"
"runtime"
"strings" "strings"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx" "github.com/joomcode/errorx"
) )
@ -58,7 +62,17 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
} }
if newconfig.Enabled { 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 { if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err) httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return 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) { func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL) log.Tracef("%s %v", r.Method, r.URL)
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
@ -147,13 +165,35 @@ func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
http.Error(w, errorText, http.StatusBadRequest) http.Error(w, errorText, http.StatusBadRequest)
return return
} }
found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName) found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName)
result := map[string]interface{}{}
if err != nil { othSrv := map[string]interface{}{}
result["error"] = err.Error() foundVal := "no"
} else { if found {
result["found"] = 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") w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result) err = json.NewEncoder(w).Encode(result)
if err != nil { 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 { func startDHCPServer() error {
if !config.DHCP.Enabled { if !config.DHCP.Enabled {
// not enabled, don't do anything // not enabled, don't do anything