Skip to content

Commit

Permalink
dhcpv4: validate subnet mask and ip range
Browse files Browse the repository at this point in the history
  • Loading branch information
105th committed Oct 18, 2021
1 parent 2b635bf commit 5798a80
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 8 deletions.
12 changes: 11 additions & 1 deletion client/src/__locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
28 changes: 24 additions & 4 deletions client/src/components/Settings/Dhcp/FormDHCPv4.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
validateIpv4,
validateRequiredValue,
validateIpv4RangeEnd,
validateGatewaySubnetMask,
validateIpForGatewaySubnetMask,
validateNotInRange,
} from '../../../helpers/validators';

const FormDHCPv4 = ({
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/Settings/Dhcp/StaticLeases/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
validateMac,
validateRequiredValue,
validateIpv4InCidr,
validateInRange,
} from '../../../../helpers/validators';
import { FORM_NAME } from '../../../../helpers/constants';
import { toggleLeaseModal } from '../../../../actions';
Expand Down Expand Up @@ -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">
Expand Down
8 changes: 8 additions & 0 deletions client/src/components/Settings/Dhcp/StaticLeases/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const Modal = ({
handleSubmit,
processingAdding,
cidr,
rangeStart,
rangeEnd,
}) => {
const dispatch = useDispatch();

Expand Down Expand Up @@ -38,10 +40,14 @@ const Modal = ({
ip: '',
hostname: '',
cidr,
rangeStart,
rangeEnd,
}}
onSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
/>
</div>
</ReactModal>
Expand All @@ -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);
6 changes: 6 additions & 0 deletions client/src/components/Settings/Dhcp/StaticLeases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const StaticLeases = ({
processingDeleting,
staticLeases,
cidr,
rangeStart,
rangeEnd,
}) => {
const [t] = useTranslation();
const dispatch = useDispatch();
Expand Down Expand Up @@ -100,6 +102,8 @@ const StaticLeases = ({
handleSubmit={handleSubmit}
processingAdding={processingAdding}
cidr={cidr}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
/>
</>
);
Expand All @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/Settings/Dhcp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions client/src/helpers/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 111 additions & 2 deletions client/src/helpers/validators.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import i18next from 'i18next';

import {
MAX_PORT,
R_CIDR,
Expand All @@ -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/
Expand Down Expand Up @@ -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;
Expand All @@ -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}
Expand Down
25 changes: 25 additions & 0 deletions internal/dhcpd/v4.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -1124,6 +1126,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 {
Expand Down

0 comments on commit 5798a80

Please sign in to comment.