2020-02-01 00:47:58 -10:00
|
|
|
"""Input validation functions for submitted queries."""
|
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
# Standard Library
|
2020-01-26 02:21:53 -07:00
|
|
|
import re
|
|
|
|
from ipaddress import ip_network
|
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
# Project
|
2020-01-31 02:06:27 -10:00
|
|
|
from hyperglass.util import log
|
2020-02-03 02:34:50 -07:00
|
|
|
from hyperglass.exceptions import InputInvalid, InputNotAllowed
|
|
|
|
from hyperglass.configuration import params
|
2020-01-26 02:21:53 -07:00
|
|
|
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
def _member_of(target, network):
|
|
|
|
"""Check if IP address belongs to network.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
target {object} -- Target IPv4/IPv6 address
|
|
|
|
network {object} -- ACL network
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
{bool} -- True if target is a member of network, False if not
|
|
|
|
"""
|
|
|
|
log.debug(f"Checking membership of {target} for {network}")
|
|
|
|
|
|
|
|
membership = False
|
|
|
|
if (
|
|
|
|
network.network_address <= target.network_address
|
|
|
|
and network.broadcast_address >= target.broadcast_address # NOQA: W503
|
|
|
|
):
|
|
|
|
log.debug(f"{target} is a member of {network}")
|
|
|
|
membership = True
|
|
|
|
return membership
|
|
|
|
|
|
|
|
|
|
|
|
def _prefix_range(target, ge, le):
|
|
|
|
"""Verify if target prefix length is within ge/le threshold.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
target {IPv4Network|IPv6Network} -- Valid IPv4/IPv6 Network
|
|
|
|
ge {int} -- Greater than
|
|
|
|
le {int} -- Less than
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
{bool} -- True if target in range; False if not
|
|
|
|
"""
|
|
|
|
matched = False
|
|
|
|
if target.prefixlen <= le and target.prefixlen >= ge:
|
|
|
|
matched = True
|
|
|
|
return matched
|
|
|
|
|
|
|
|
|
|
|
|
def validate_ip(value, query_type, query_vrf): # noqa: C901
|
2020-01-26 02:21:53 -07:00
|
|
|
"""Ensure input IP address is both valid and not within restricted allocations.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
value {str} -- Unvalidated IP Address
|
2020-01-31 02:06:27 -10:00
|
|
|
query_type {str} -- Valid query type
|
|
|
|
query_vrf {object} -- Matched query vrf
|
2020-01-26 02:21:53 -07:00
|
|
|
Raises:
|
|
|
|
ValueError: Raised if input IP address is not an IP address.
|
|
|
|
ValueError: Raised if IP address is valid, but is within a restricted range.
|
|
|
|
Returns:
|
|
|
|
Union[IPv4Address, IPv6Address] -- Validated IP address object
|
|
|
|
"""
|
2020-01-28 10:16:18 -07:00
|
|
|
query_type_params = getattr(params.queries, query_type)
|
2020-01-26 02:21:53 -07:00
|
|
|
try:
|
|
|
|
|
|
|
|
# Attempt to use IP object factory to create an IP address object
|
|
|
|
valid_ip = ip_network(value)
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
raise InputInvalid(
|
|
|
|
params.messages.invalid_input,
|
|
|
|
target=value,
|
|
|
|
query_type=query_type_params.display_name,
|
|
|
|
)
|
|
|
|
|
2020-03-22 08:11:56 -07:00
|
|
|
# Test the valid IP address to determine if it is:
|
|
|
|
# - Unspecified (See RFC5735, RFC2373)
|
|
|
|
# - Loopback (See RFC5735, RFC2373)
|
|
|
|
# - Otherwise IETF Reserved
|
|
|
|
# ...and returns an error if so.
|
2020-01-26 02:21:53 -07:00
|
|
|
if valid_ip.is_reserved or valid_ip.is_unspecified or valid_ip.is_loopback:
|
|
|
|
raise InputInvalid(
|
|
|
|
params.messages.invalid_input,
|
|
|
|
target=value,
|
|
|
|
query_type=query_type_params.display_name,
|
|
|
|
)
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
ip_version = valid_ip.version
|
2020-03-21 15:23:32 -07:00
|
|
|
|
2020-03-22 08:11:56 -07:00
|
|
|
if getattr(query_vrf, f"ipv{ip_version}") is None:
|
|
|
|
raise InputInvalid(
|
|
|
|
params.messages.feature_not_enabled,
|
|
|
|
feature=f"IPv{ip_version}",
|
|
|
|
device_name=f"VRF {query_vrf.display_name}",
|
|
|
|
)
|
|
|
|
|
2020-03-22 17:46:08 -07:00
|
|
|
vrf_afi = getattr(query_vrf, f"ipv{ip_version}")
|
2020-03-21 15:23:32 -07:00
|
|
|
|
2020-03-22 17:46:08 -07:00
|
|
|
for ace in [a for a in vrf_afi.access_list if a.network.version == ip_version]:
|
2020-03-21 15:23:32 -07:00
|
|
|
if _member_of(valid_ip, ace.network):
|
|
|
|
if query_type == "bgp_route" and _prefix_range(valid_ip, ace.ge, ace.le):
|
|
|
|
pass
|
|
|
|
|
|
|
|
if ace.action == "permit":
|
|
|
|
log.debug(
|
|
|
|
"{t} is allowed by access-list {a}", t=str(valid_ip), a=repr(ace)
|
|
|
|
)
|
|
|
|
break
|
|
|
|
elif ace.action == "deny":
|
|
|
|
raise InputNotAllowed(
|
|
|
|
params.messages.acl_denied,
|
|
|
|
target=str(valid_ip),
|
|
|
|
denied_network=str(ace.network),
|
|
|
|
)
|
2020-01-26 02:21:53 -07:00
|
|
|
if valid_ip.num_addresses == 1:
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
if query_type in ("ping", "traceroute"):
|
|
|
|
new_ip = valid_ip.network_address
|
|
|
|
|
|
|
|
log.debug(
|
|
|
|
"Converted '{o}' to '{n}' for '{q}' query",
|
|
|
|
o=valid_ip,
|
|
|
|
n=new_ip,
|
|
|
|
q=query_type,
|
|
|
|
)
|
|
|
|
|
|
|
|
valid_ip = new_ip
|
|
|
|
|
2020-03-22 17:46:08 -07:00
|
|
|
elif query_type in ("bgp_route",) and vrf_afi.force_cidr:
|
2020-01-31 02:06:27 -10:00
|
|
|
max_le = max(
|
|
|
|
ace.le
|
|
|
|
for ace in query_vrf[ip_version].access_list
|
|
|
|
if ace.action == "permit"
|
|
|
|
)
|
|
|
|
new_ip = valid_ip.supernet(new_prefix=max_le)
|
|
|
|
|
|
|
|
log.debug(
|
|
|
|
"Converted '{o}' to '{n}' for '{q}' query",
|
|
|
|
o=valid_ip,
|
|
|
|
n=new_ip,
|
|
|
|
q=query_type,
|
|
|
|
)
|
|
|
|
|
|
|
|
valid_ip = new_ip
|
2020-03-22 17:46:08 -07:00
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
log.debug("Validation passed for {ip}", ip=value)
|
2020-01-26 02:21:53 -07:00
|
|
|
return valid_ip
|
|
|
|
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
def validate_community(value):
|
2020-01-26 02:21:53 -07:00
|
|
|
"""Validate input communities against configured or default regex pattern."""
|
|
|
|
|
|
|
|
# RFC4360: Extended Communities (New Format)
|
2020-01-31 02:06:27 -10:00
|
|
|
if re.match(params.queries.bgp_community.pattern.extended_as, value):
|
2020-01-26 02:21:53 -07:00
|
|
|
pass
|
|
|
|
|
|
|
|
# RFC4360: Extended Communities (32 Bit Format)
|
2020-01-31 02:06:27 -10:00
|
|
|
elif re.match(params.queries.bgp_community.pattern.decimal, value):
|
2020-01-26 02:21:53 -07:00
|
|
|
pass
|
|
|
|
|
|
|
|
# RFC8092: Large Communities
|
2020-01-31 02:06:27 -10:00
|
|
|
elif re.match(params.queries.bgp_community.pattern.large, value):
|
2020-01-26 02:21:53 -07:00
|
|
|
pass
|
|
|
|
|
|
|
|
else:
|
|
|
|
raise InputInvalid(
|
|
|
|
params.messages.invalid_input,
|
|
|
|
target=value,
|
2020-01-28 10:16:18 -07:00
|
|
|
query_type=params.queries.bgp_community.display_name,
|
2020-01-26 02:21:53 -07:00
|
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
def validate_aspath(value):
|
2020-01-26 02:21:53 -07:00
|
|
|
"""Validate input AS_PATH against configured or default regext pattern."""
|
|
|
|
|
2020-01-31 02:06:27 -10:00
|
|
|
mode = params.queries.bgp_aspath.pattern.mode
|
|
|
|
pattern = getattr(params.queries.bgp_aspath.pattern, mode)
|
2020-01-26 02:21:53 -07:00
|
|
|
|
|
|
|
if not re.match(pattern, value):
|
|
|
|
raise InputInvalid(
|
|
|
|
params.messages.invalid_input,
|
|
|
|
target=value,
|
2020-01-28 10:16:18 -07:00
|
|
|
query_type=params.queries.bgp_aspath.display_name,
|
2020-01-26 02:21:53 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
return value
|