Pull request #1329: 3529 validate dhcpv4

Merge in DNS/adguard-home from 3529-validate-dhcpv4 to master

Squashed commit of the following:

commit 2f2455aa13a41398cd2846f31be96da9d34ba95d
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Tue Oct 19 19:18:12 2021 +0300

    dhcpv4: better test && fix changelog

commit ec4ff9180e8390fb739b3be0fc76fd2c715fe691
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 19:08:44 2021 +0300

    dhcpv4: better tests

commit e0e2f27b7a063ed84af170b16c3f87636cb738d2
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 18:55:47 2021 +0300

    dhcpv4: better tests

commit 73e1d08e1265e336ee6339d5021f90883fe3e395
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 18:47:21 2021 +0300

    dhcpv4: better tests

commit f636fc316123f26b6e2930afb4b22c18024ec93d
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 18:47:07 2021 +0300

    all: updated golibs

commit 86dd107a1d483ac24bd8c26422324eb8b9c3d086
Merge: 51aaf6d9 b296fa22
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 17:18:17 2021 +0300

    Merge branch 'master' into 3529-validate-dhcpv4

commit 51aaf6d9eb5fbe2b4304254dc6782305a19c53fa
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 17:18:02 2021 +0300

    dhcpv4: better changelog

commit 720b896bb595c57fab6d376f88c8a4b1d131db40
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 17:14:25 2021 +0300

    dhcpv4: better tests

commit 1098beffca8d5feb2ec104d26419210962c9a97d
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 12:08:26 2021 +0300

    dhcp: changelog

commit d1f6c89d68657431fb261658133c67e9e3135c1c
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 12:03:06 2021 +0300

    dhcpv4: fixed tests

commit 8b6713468fc04321c5238300df90bbb2d67ee679
Merge: 9991e9cb 3fa38fb4
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 11:57:57 2021 +0300

    Merge branch 'master' into 3529-validate-dhcpv4

commit 9991e9cbee7dc87d8fa1d7e86e6cc7e09ab6938c
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 11:55:40 2021 +0300

    dhcpv4: added tests

commit 5798a80de6c060365c1c647326d46cc13ccf28cb
Author: Dmitriy Seregin <d.seregin@adguard.com>
Date:   Mon Oct 18 11:46:03 2021 +0300

    dhcpv4: validate subnet mask and ip range
This commit is contained in:
Dmitry Seregin 2021-10-19 19:28:18 +03:00
parent b296fa2246
commit d7aafa7dc6
13 changed files with 258 additions and 11 deletions

View File

@ -46,6 +46,8 @@ and this project adheres to
### Changed
- DHCP gateway address, subnet mask, IP address range, and leases validations
([#3529]).
- The `systemd` service script will now create the `/var/log` directory when it
doesn't exist ([#3579]).
- Items in allowed clients, disallowed clients, and blocked hosts lists are now
@ -196,6 +198,7 @@ In this release, the schema version has changed from 10 to 12.
[#3450]: https://github.com/AdguardTeam/AdGuardHome/issues/3450
[#3457]: https://github.com/AdguardTeam/AdGuardHome/issues/3457
[#3506]: https://github.com/AdguardTeam/AdGuardHome/issues/3506
[#3529]: https://github.com/AdguardTeam/AdGuardHome/issues/3529
[#3538]: https://github.com/AdguardTeam/AdGuardHome/issues/3538
[#3551]: https://github.com/AdguardTeam/AdGuardHome/issues/3551
[#3564]: https://github.com/AdguardTeam/AdGuardHome/issues/3564

View File

@ -37,6 +37,9 @@
"dhcp_ipv6_settings": "DHCP IPv6 Settings",
"form_error_required": "Required field",
"form_error_ip4_format": "Invalid IPv4 format",
"form_error_ip4_range_start_format": "Invalid range start IPv4 format",
"form_error_ip4_range_end_format": "Invalid range end IPv4 format",
"form_error_ip4_gateway_format": "Invalid gateway IPv4 format",
"form_error_ip6_format": "Invalid IPv6 format",
"form_error_ip_format": "Invalid IP format",
"form_error_mac_format": "Invalid MAC format",
@ -45,7 +48,14 @@
"form_error_subnet": "Subnet \"{{cidr}}\" does not contain the IP address \"{{ip}}\"",
"form_error_positive": "Must be greater than 0",
"form_error_negative": "Must be equal to 0 or greater",
"range_end_error": "Must be greater than range start",
"out_of_range_error": "Must be out of range \"{{start}}\"-\"{{end}}\"",
"in_range_error": "Must be in range \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Must be lower than range start",
"lower_range_end_error": "Must be lower than range end",
"greater_range_start_error": "Must be greater than range start",
"greater_range_end_error": "Must be greater than range end",
"subnet_error": "Addresses must be in one subnet",
"gateway_or_subnet_invalid": "Subnet mask invalid",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Range of IP addresses",

View File

@ -13,6 +13,9 @@ import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';
const FormDHCPv4 = ({
@ -54,7 +57,11 @@ const FormDHCPv4 = ({
type="text"
className="form-control"
placeholder={t(ipv4placeholders.gateway_ip)}
validate={[validateIpv4, validateRequired]}
validate={[
validateIpv4,
validateRequired,
validateNotInRange,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
@ -66,7 +73,11 @@ const FormDHCPv4 = ({
type="text"
className="form-control"
placeholder={t(ipv4placeholders.subnet_mask)}
validate={[validateIpv4, validateRequired]}
validate={[
validateIpv4,
validateRequired,
validateGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
@ -84,7 +95,11 @@ const FormDHCPv4 = ({
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_start)}
validate={[validateIpv4]}
validate={[
validateIpv4,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>
@ -95,7 +110,12 @@ const FormDHCPv4 = ({
type="text"
className="form-control"
placeholder={t(ipv4placeholders.range_end)}
validate={[validateIpv4, validateIpv4RangeEnd]}
validate={[
validateIpv4,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
]}
disabled={!isInterfaceIncludesIpv4}
/>
</div>

View File

@ -10,6 +10,7 @@ import {
validateMac,
validateRequiredValue,
validateIpv4InCidr,
validateInRange,
} from '../../../../helpers/validators';
import { FORM_NAME } from '../../../../helpers/constants';
import { toggleLeaseModal } from '../../../../actions';
@ -53,7 +54,12 @@ const Form = ({
type="text"
className="form-control"
placeholder={t('form_enter_subnet_ip', { cidr })}
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr]}
validate={[
validateRequiredValue,
validateIpv4,
validateIpv4InCidr,
validateInRange,
]}
/>
</div>
<div className="form__group">

View File

@ -11,6 +11,8 @@ const Modal = ({
handleSubmit,
processingAdding,
cidr,
rangeStart,
rangeEnd,
}) => {
const dispatch = useDispatch();
@ -38,10 +40,14 @@ const Modal = ({
ip: '',
hostname: '',
cidr,
rangeStart,
rangeEnd,
}}
onSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
/>
</div>
</ReactModal>
@ -53,6 +59,8 @@ Modal.propTypes = {
handleSubmit: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
};
export default withTranslation()(Modal);

View File

@ -22,6 +22,8 @@ const StaticLeases = ({
processingDeleting,
staticLeases,
cidr,
rangeStart,
rangeEnd,
}) => {
const [t] = useTranslation();
const dispatch = useDispatch();
@ -100,6 +102,8 @@ const StaticLeases = ({
handleSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
/>
</>
);
@ -111,6 +115,8 @@ StaticLeases.propTypes = {
processingAdding: PropTypes.bool.isRequired,
processingDeleting: PropTypes.bool.isRequired,
cidr: PropTypes.string.isRequired,
rangeStart: PropTypes.string,
rangeEnd: PropTypes.string,
};
cellWrap.propTypes = {

View File

@ -275,6 +275,8 @@ const Dhcp = () => {
processingAdding={processingAdding}
processingDeleting={processingDeleting}
cidr={cidr}
rangeStart={dhcp?.values?.v4?.range_start}
rangeEnd={dhcp?.values?.v4?.range_end}
/>
<div className="btn-list mt-2">
<button

View File

@ -552,6 +552,20 @@ export const isIpInCidr = (ip, cidr) => {
}
};
/**
*
* @param {string} subnetMask
* @returns {IPv4 | null}
*/
export const parseSubnetMask = (subnetMask) => {
try {
return ipaddr.parse(subnetMask).prefixLengthFromSubnetMask();
} catch (e) {
console.error(e);
return null;
}
};
/**
*
* @param {string} subnetMask

View File

@ -1,4 +1,5 @@
import i18next from 'i18next';
import {
MAX_PORT,
R_CIDR,
@ -14,7 +15,7 @@ import {
R_DOMAIN,
} from './constants';
import { ip4ToInt, isValidAbsolutePath } from './form';
import { isIpInCidr } from './helpers';
import { isIpInCidr, parseSubnetMask } from './helpers';
// Validation functions
// https://redux-form.com/8.3.0/examples/fieldlevelvalidation/
@ -44,7 +45,7 @@ export const validateIpv4RangeEnd = (_, allValues) => {
const { range_end, range_start } = allValues.v4;
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
return 'range_end_error';
return 'greater_range_start_error';
}
return undefined;
@ -61,6 +62,114 @@ export const validateIpv4 = (value) => {
return undefined;
};
/**
* @returns {undefined|string}
* @param _
* @param allValues
*/
export const validateNotInRange = (value, allValues) => {
const { range_start, range_end } = allValues.v4;
if (range_start && validateIpv4(range_start)) {
return 'form_error_ip4_range_start_format';
}
if (range_end && validateIpv4(range_end)) {
return 'form_error_ip4_range_end_format';
}
const isAboveMin = range_start && ip4ToInt(value) >= ip4ToInt(range_start);
const isBelowMax = range_end && ip4ToInt(value) <= ip4ToInt(range_end);
if (isAboveMin && isBelowMax) {
return i18next.t('out_of_range_error', {
start: range_start,
end: range_end,
});
}
if (!range_end && isAboveMin) {
return 'lower_range_start_error';
}
if (!range_start && isBelowMax) {
return 'greater_range_end_error';
}
return undefined;
};
/**
* @returns {undefined|string}
* @param _
* @param allValues
*/
export const validateInRange = (value, allValues) => {
const { rangeStart, rangeEnd } = allValues;
if (rangeStart && validateIpv4(rangeStart)) {
return 'form_error_ip4_range_start_format';
}
if (rangeEnd && validateIpv4(rangeEnd)) {
return 'form_error_ip4_range_end_format';
}
const isBelowMin = rangeStart && ip4ToInt(value) < ip4ToInt(rangeStart);
const isAboveMax = rangeEnd && ip4ToInt(value) > ip4ToInt(rangeEnd);
if (isAboveMax || isBelowMin) {
return i18next.t('in_range_error', {
start: rangeStart,
end: rangeEnd,
});
}
return undefined;
};
/**
* @returns {undefined|string}
* @param _
* @param allValues
*/
export const validateGatewaySubnetMask = (_, allValues) => {
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
return 'gateway_or_subnet_invalid';
}
const { subnet_mask, gateway_ip } = allValues.v4;
if (validateIpv4(gateway_ip)) {
return 'form_error_ip4_gateway_format';
}
return parseSubnetMask(subnet_mask) ? undefined : 'gateway_or_subnet_invalid';
};
/**
* @returns {undefined|string}
* @param value
* @param allValues
*/
export const validateIpForGatewaySubnetMask = (value, allValues) => {
if (!allValues || !allValues.v4 || !value) {
return undefined;
}
const {
gateway_ip, subnet_mask,
} = allValues.v4;
const subnetPrefix = parseSubnetMask(subnet_mask);
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
return 'subnet_error';
}
return undefined;
};
/**
* @param value {string}
* @returns {undefined|string}

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.16
require (
github.com/AdguardTeam/dnsproxy v0.39.8
github.com/AdguardTeam/golibs v0.10.0
github.com/AdguardTeam/golibs v0.10.2
github.com/AdguardTeam/urlfilter v0.14.6
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.2

4
go.sum
View File

@ -14,8 +14,8 @@ github.com/AdguardTeam/dnsproxy v0.39.8/go.mod h1:eDpJKAdkHORRwAedjuERv+7SWlcz4c
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.9.2/go.mod h1:fCAMwPBJ8S7YMYbTWvYS+eeTLblP5E04IDtNAo7y7IY=
github.com/AdguardTeam/golibs v0.10.0 h1:A7MXRfZ+ItpOyS9tWKtqrLj3vZtE9FJFC+dOVY/LcWs=
github.com/AdguardTeam/golibs v0.10.0/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.2 h1:TAwnS4Y49sSUa4UX1yz/MWNGbIlXHqafrWr9MxdIh9A=
github.com/AdguardTeam/golibs v0.10.2/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.14.6 h1:emqoKZElooHACYehRBYENeKVN1a/rspxiqTIMYLuoIo=
github.com/AdguardTeam/urlfilter v0.14.6/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=

View File

@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -138,6 +139,49 @@ func TestNormalizeLeases(t *testing.T) {
assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
}
func TestV4Server_badRange(t *testing.T) {
testCases := []struct {
name string
gatewayIP net.IP
subnetMask net.IP
wantErrMsg string
}{{
name: "gateway_in_range",
gatewayIP: net.IP{192, 168, 10, 120},
subnetMask: net.IP{255, 255, 255, 0},
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
"192.168.10.20-192.168.10.200",
}, {
name: "outside_range_start",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 240},
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
"192.168.10.1/28",
}, {
name: "outside_range_end",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 224},
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
"192.168.10.1/27",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
RangeStart: net.IP{192, 168, 10, 20},
RangeEnd: net.IP{192, 168, 10, 200},
GatewayIP: tc.gatewayIP,
SubnetMask: tc.subnetMask,
notify: testNotify,
}
_, err := v4Create(conf)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}
// cloneUDPAddr returns a deep copy of a.
func cloneUDPAddr(a *net.UDPAddr) (clone *net.UDPAddr) {
return &net.UDPAddr{

View File

@ -293,6 +293,8 @@ func (s *v4Server) addLease(l *Lease) (err error) {
offset, inOffset := r.offset(l.IP)
if l.IsStatic() {
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
// disabled.
if sn := s.conf.subnet; !sn.Contains(l.IP) {
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
}
@ -1125,6 +1127,29 @@ func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
return s, fmt.Errorf("dhcpv4: %w", err)
}
if s.conf.ipRange.contains(routerIP) {
return s, fmt.Errorf("dhcpv4: gateway ip %v in the ip range: %v-%v",
routerIP,
conf.RangeStart,
conf.RangeEnd,
)
}
if !s.conf.subnet.Contains(conf.RangeStart) {
return s, fmt.Errorf("dhcpv4: range start %v is outside network %v",
conf.RangeStart,
s.conf.subnet,
)
}
if !s.conf.subnet.Contains(conf.RangeEnd) {
return s, fmt.Errorf("dhcpv4: range end %v is outside network %v",
conf.RangeEnd,
s.conf.subnet,
)
}
// TODO(a.garipov, d.seregin): Check that every lease is inside the IPRange.
s.leasedOffsets = newBitSet()
if conf.LeaseDuration == 0 {