acl: rework source/destination
For ACE 'source' and 'destination' is now possible to specify one of: - ipv4 or ipv6 address - ipv4 or ipv6 prefix - name of a prefixlist The validator resolves the src/dst network list, optionally filtering this with the desired 'family' (which defaults to 'any'). Errors are raised if the resulting src/dst network lists do not overlap, that is to say if all src entries are IPv4 but there are no IPv4 dst entries and vise-versa. * Update the example to have a 'trusted' prefixlist. * Update the unit tests to use the new error message(s).
This commit is contained in:
@ -40,28 +40,12 @@ def get_by_name(yaml, aclname):
|
|||||||
def hydrate_term(acl_term):
|
def hydrate_term(acl_term):
|
||||||
"""Adds all defaults to an ACL term"""
|
"""Adds all defaults to an ACL term"""
|
||||||
|
|
||||||
# Figure out the address family
|
|
||||||
if "family" not in acl_term:
|
if "family" not in acl_term:
|
||||||
if "source" in acl_term and ":" in acl_term["source"]:
|
|
||||||
acl_term["family"] = "ipv6"
|
|
||||||
elif "destination" in acl_term and ":" in acl_term["destination"]:
|
|
||||||
acl_term["family"] = "ipv6"
|
|
||||||
elif "source" in acl_term and "." in acl_term["source"]:
|
|
||||||
acl_term["family"] = "ipv4"
|
|
||||||
elif "destination" in acl_term and "." in acl_term["destination"]:
|
|
||||||
acl_term["family"] = "ipv4"
|
|
||||||
else:
|
|
||||||
acl_term["family"] = "any"
|
acl_term["family"] = "any"
|
||||||
|
if "source" not in acl_term:
|
||||||
# Set source/destionation based on family, if they're omitted
|
acl_term["source"] = "any"
|
||||||
if acl_term["family"] == "ipv4" and "source" not in acl_term:
|
if "destination" not in acl_term:
|
||||||
acl_term["source"] = "0.0.0.0/0"
|
acl_term["destination"] = "any"
|
||||||
if acl_term["family"] == "ipv4" and "destination" not in acl_term:
|
|
||||||
acl_term["destination"] = "0.0.0.0/0"
|
|
||||||
if acl_term["family"] == "ipv6" and "source" not in acl_term:
|
|
||||||
acl_term["source"] = "::/0"
|
|
||||||
if acl_term["family"] == "ipv6" and "destination" not in acl_term:
|
|
||||||
acl_term["destination"] = "::/0"
|
|
||||||
|
|
||||||
if "protocol" not in acl_term or acl_term["protocol"] == "any":
|
if "protocol" not in acl_term or acl_term["protocol"] == "any":
|
||||||
acl_term["protocol"] = 0
|
acl_term["protocol"] = 0
|
||||||
@ -181,6 +165,13 @@ def get_network_list(yaml, network_string, want_ipv4=True, want_ipv6=True):
|
|||||||
ret = [ipn]
|
ret = [ipn]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
if network_string == "any":
|
||||||
|
if want_ipv4:
|
||||||
|
ret.append(ipaddress.ip_network("0.0.0.0/0"))
|
||||||
|
if want_ipv6:
|
||||||
|
ret.append(ipaddress.ip_network("::/0"))
|
||||||
|
return ret
|
||||||
|
|
||||||
return prefixlist.get_network_list(
|
return prefixlist.get_network_list(
|
||||||
yaml, network_string, want_ipv4=want_ipv4, want_ipv6=want_ipv6
|
yaml, network_string, want_ipv4=want_ipv4, want_ipv6=want_ipv6
|
||||||
)
|
)
|
||||||
@ -211,6 +202,16 @@ def get_protocol(protostring):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def network_list_has_family(network_list, version):
|
||||||
|
"""Returns True if the given list of ip_network() elements has at least one
|
||||||
|
element with the specified version, which can be either 4 or 6. Return False
|
||||||
|
otherwise"""
|
||||||
|
for m in network_list:
|
||||||
|
if m.version == version:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_acls(yaml):
|
def validate_acls(yaml):
|
||||||
"""Validate the semantics of all YAML 'acls' entries"""
|
"""Validate the semantics of all YAML 'acls' entries"""
|
||||||
result = True
|
result = True
|
||||||
@ -230,25 +231,53 @@ def validate_acls(yaml):
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f"acl {aclname} term {terms} orig {orig_acl_term} hydrated {acl_term}"
|
f"acl {aclname} term {terms} orig {orig_acl_term} hydrated {acl_term}"
|
||||||
)
|
)
|
||||||
if acl_term["family"] == "any":
|
if acl_term["family"] == "ipv4":
|
||||||
if "source" in acl_term:
|
want_ipv4 = True
|
||||||
msgs.append(
|
want_ipv6 = False
|
||||||
f"acl {aclname} term {terms} family any cannot have source"
|
elif acl_term["family"] == "ipv6":
|
||||||
)
|
want_ipv4 = False
|
||||||
result = False
|
want_ipv6 = True
|
||||||
if "destination" in acl_term:
|
|
||||||
msgs.append(
|
|
||||||
f"acl {aclname} term {terms} family any cannot have destination"
|
|
||||||
)
|
|
||||||
result = False
|
|
||||||
else:
|
else:
|
||||||
src = ipaddress.ip_network(acl_term["source"])
|
want_ipv4 = True
|
||||||
dst = ipaddress.ip_network(acl_term["destination"])
|
want_ipv6 = True
|
||||||
if src.version != dst.version:
|
|
||||||
|
src_network_list = get_network_list(
|
||||||
|
yaml, acl_term["source"], want_ipv4=want_ipv4, want_ipv6=want_ipv6
|
||||||
|
)
|
||||||
|
dst_network_list = get_network_list(
|
||||||
|
yaml, acl_term["destination"], want_ipv4=want_ipv4, want_ipv6=want_ipv6
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"acl {aclname} term {terms} src: {src_network_list} dst: {dst_network_list}"
|
||||||
|
)
|
||||||
|
if len(src_network_list) == 0:
|
||||||
msgs.append(
|
msgs.append(
|
||||||
f"acl {aclname} term {terms} source and destination have different address family"
|
f"acl {aclname} term {terms} family {acl_term['family']} has no source"
|
||||||
)
|
)
|
||||||
result = False
|
result = False
|
||||||
|
if len(dst_network_list) == 0:
|
||||||
|
msgs.append(
|
||||||
|
f"acl {aclname} term {terms} family {acl_term['family']} has no destination"
|
||||||
|
)
|
||||||
|
result = False
|
||||||
|
if len(dst_network_list) == 0 or len(src_network_list) == 0:
|
||||||
|
## Pointless to continue if there's no src/dst at all
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_network_has_ipv4 = network_list_has_family(src_network_list, 4)
|
||||||
|
dst_network_has_ipv4 = network_list_has_family(dst_network_list, 4)
|
||||||
|
src_network_has_ipv6 = network_list_has_family(src_network_list, 6)
|
||||||
|
dst_network_has_ipv6 = network_list_has_family(dst_network_list, 6)
|
||||||
|
|
||||||
|
if (
|
||||||
|
src_network_has_ipv4 != dst_network_has_ipv4
|
||||||
|
and src_network_has_ipv6 != dst_network_has_ipv6
|
||||||
|
):
|
||||||
|
msgs.append(
|
||||||
|
f"acl {aclname} term {terms} source and destination family do not overlap"
|
||||||
|
)
|
||||||
|
result = False
|
||||||
|
continue
|
||||||
|
|
||||||
proto = get_protocol(acl_term["protocol"])
|
proto = get_protocol(acl_term["protocol"])
|
||||||
if proto is None:
|
if proto is None:
|
||||||
@ -266,8 +295,7 @@ def validate_acls(yaml):
|
|||||||
f"acl {aclname} term {terms} destination-port can only be specified for protocol tcp or udp"
|
f"acl {aclname} term {terms} destination-port can only be specified for protocol tcp or udp"
|
||||||
)
|
)
|
||||||
result = False
|
result = False
|
||||||
|
else:
|
||||||
if proto in [6, 17]:
|
|
||||||
src_low_port, src_high_port = get_port_low_high(acl_term["source-port"])
|
src_low_port, src_high_port = get_port_low_high(acl_term["source-port"])
|
||||||
dst_low_port, dst_high_port = get_port_low_high(
|
dst_low_port, dst_high_port = get_port_low_high(
|
||||||
acl_term["destination-port"]
|
acl_term["destination-port"]
|
||||||
@ -328,7 +356,7 @@ def validate_acls(yaml):
|
|||||||
f"acl {aclname} term {terms} icmp-type can only be specified for protocol icmp or icmp-ipv6"
|
f"acl {aclname} term {terms} icmp-type can only be specified for protocol icmp or icmp-ipv6"
|
||||||
)
|
)
|
||||||
result = False
|
result = False
|
||||||
if proto in [1, 58]:
|
else:
|
||||||
icmp_code_low, icmp_code_high = get_icmp_low_high(acl_term["icmp-code"])
|
icmp_code_low, icmp_code_high = get_icmp_low_high(acl_term["icmp-code"])
|
||||||
icmp_type_low, icmp_type_high = get_icmp_low_high(acl_term["icmp-type"])
|
icmp_type_low, icmp_type_high = get_icmp_low_high(acl_term["icmp-type"])
|
||||||
if icmp_code_low > icmp_code_high:
|
if icmp_code_low > icmp_code_high:
|
||||||
|
@ -152,3 +152,20 @@ class TestACLMethods(unittest.TestCase):
|
|||||||
l = acl.get_network_list(self.cfg, "pl-notexist")
|
l = acl.get_network_list(self.cfg, "pl-notexist")
|
||||||
self.assertIsInstance(l, list)
|
self.assertIsInstance(l, list)
|
||||||
self.assertEquals(0, len(l))
|
self.assertEquals(0, len(l))
|
||||||
|
|
||||||
|
def test_network_list_has_family(self):
|
||||||
|
l = acl.get_network_list(self.cfg, "trusted")
|
||||||
|
self.assertTrue(acl.network_list_has_family(l, 4))
|
||||||
|
self.assertTrue(acl.network_list_has_family(l, 6))
|
||||||
|
|
||||||
|
l = acl.get_network_list(self.cfg, "trusted", want_ipv4=False)
|
||||||
|
self.assertFalse(acl.network_list_has_family(l, 4))
|
||||||
|
self.assertTrue(acl.network_list_has_family(l, 6))
|
||||||
|
|
||||||
|
l = acl.get_network_list(self.cfg, "trusted", want_ipv6=False)
|
||||||
|
self.assertTrue(acl.network_list_has_family(l, 4))
|
||||||
|
self.assertFalse(acl.network_list_has_family(l, 6))
|
||||||
|
|
||||||
|
l = acl.get_network_list(self.cfg, "trusted", want_ipv4=False, want_ipv6=False)
|
||||||
|
self.assertFalse(acl.network_list_has_family(l, 4))
|
||||||
|
self.assertFalse(acl.network_list_has_family(l, 6))
|
||||||
|
@ -126,6 +126,7 @@ prefixlists:
|
|||||||
- 192.0.2.0/24
|
- 192.0.2.0/24
|
||||||
- 2001:db8::1
|
- 2001:db8::1
|
||||||
- 2001:db8::/64
|
- 2001:db8::/64
|
||||||
|
- 2001:db8::/48
|
||||||
empty:
|
empty:
|
||||||
description: "An empty prefixlist"
|
description: "An empty prefixlist"
|
||||||
members: []
|
members: []
|
||||||
@ -134,6 +135,13 @@ acls:
|
|||||||
acl01:
|
acl01:
|
||||||
description: "Test ACL"
|
description: "Test ACL"
|
||||||
terms:
|
terms:
|
||||||
|
- description: "Allow a prefixlist"
|
||||||
|
action: permit
|
||||||
|
source: trusted
|
||||||
|
- description: "Allow a prefixlist for one family only"
|
||||||
|
family: ipv4
|
||||||
|
action: permit
|
||||||
|
source: trusted
|
||||||
- description: "Allow a specific IPv6 TCP flow"
|
- description: "Allow a specific IPv6 TCP flow"
|
||||||
action: permit
|
action: permit
|
||||||
source: 2001:db8::/64
|
source: 2001:db8::/64
|
||||||
|
@ -5,7 +5,7 @@ bridgedomains: map(include('bridgedomain'),key=str(matches='bd[0-9]+'),required=
|
|||||||
vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),required=False)
|
vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),required=False)
|
||||||
taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False)
|
taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False)
|
||||||
prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=64),required=False)
|
prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=64),required=False)
|
||||||
acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+'),required=False)
|
acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=56),required=False)
|
||||||
---
|
---
|
||||||
vxlan:
|
vxlan:
|
||||||
description: str(exclude='\'"',len=64,required=False)
|
description: str(exclude='\'"',len=64,required=False)
|
||||||
@ -96,8 +96,8 @@ acl-term:
|
|||||||
description: str(exclude='\'"',len=64,required=False)
|
description: str(exclude='\'"',len=64,required=False)
|
||||||
action: enum('permit','deny','permit+reflect')
|
action: enum('permit','deny','permit+reflect')
|
||||||
family: enum('ipv4','ipv6','any',required=False)
|
family: enum('ipv4','ipv6','any',required=False)
|
||||||
source: any(ip(), ip_interface(),required=False)
|
source: any(ip(),ip_interface(),str(min=1,max=56),required=False)
|
||||||
destination: any(ip(), ip_interface(),required=False)
|
destination: any(ip(),ip_interface(),str(min=1,max=56),required=False)
|
||||||
protocol: any(int(min=1,max=255),regex('^[a-z][a-z0-9-]*$'),required=False)
|
protocol: any(int(min=1,max=255),regex('^[a-z][a-z0-9-]*$'),required=False)
|
||||||
source-port: include('acl-term-port-int-range-symbolic',required=False)
|
source-port: include('acl-term-port-int-range-symbolic',required=False)
|
||||||
destination-port: include('acl-term-port-int-range-symbolic',required=False)
|
destination-port: include('acl-term-port-int-range-symbolic',required=False)
|
||||||
|
@ -3,10 +3,23 @@ test:
|
|||||||
errors:
|
errors:
|
||||||
count: 0
|
count: 0
|
||||||
---
|
---
|
||||||
|
prefixlists:
|
||||||
|
trusted:
|
||||||
|
description: "Trusted IPv4 nd IPv6 hosts"
|
||||||
|
members:
|
||||||
|
- 192.0.2.1
|
||||||
|
- 192.0.2.0/24
|
||||||
|
- 2001:db8::1
|
||||||
|
- 2001:db8::/64
|
||||||
|
- 2001:db8::/48
|
||||||
|
|
||||||
acls:
|
acls:
|
||||||
acl01:
|
acl01:
|
||||||
description: "Test ACL"
|
description: "Test ACL"
|
||||||
terms:
|
terms:
|
||||||
|
- description: "Allow a prefixlist"
|
||||||
|
action: permit
|
||||||
|
source: trusted
|
||||||
- description: "Allow a specific IPv6 TCP flow"
|
- description: "Allow a specific IPv6 TCP flow"
|
||||||
action: permit
|
action: permit
|
||||||
source: 2001:db8::/64
|
source: 2001:db8::/64
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
test:
|
|
||||||
description: "Family any precludes source/destination"
|
|
||||||
errors:
|
|
||||||
expected:
|
|
||||||
- "acl .* term .* family any cannot have (source|destination)"
|
|
||||||
count: 4
|
|
||||||
---
|
|
||||||
acls:
|
|
||||||
acl01:
|
|
||||||
terms:
|
|
||||||
- family: any
|
|
||||||
source: 0.0.0.0/0
|
|
||||||
action: permit
|
|
||||||
- family: any
|
|
||||||
source: ::/0
|
|
||||||
action: permit
|
|
||||||
- family: any
|
|
||||||
destination: 0.0.0.0/0
|
|
||||||
action: permit
|
|
||||||
- family: any
|
|
||||||
destination: ::/0
|
|
||||||
action: permit
|
|
@ -2,9 +2,20 @@ test:
|
|||||||
description: "Source and Destination must have the same address family"
|
description: "Source and Destination must have the same address family"
|
||||||
errors:
|
errors:
|
||||||
expected:
|
expected:
|
||||||
- "acl .* term .* source and destination have different address family"
|
- "acl .* term .* source and destination family do not overlap"
|
||||||
count: 4
|
count: 6
|
||||||
---
|
---
|
||||||
|
prefixlists:
|
||||||
|
v4only:
|
||||||
|
members:
|
||||||
|
- 192.0.2.1
|
||||||
|
- 192.0.2.0/24
|
||||||
|
v6only:
|
||||||
|
members:
|
||||||
|
- 2001:db8::1
|
||||||
|
- 2001:db8::/64
|
||||||
|
- 2001:db8::/48
|
||||||
|
|
||||||
acls:
|
acls:
|
||||||
acl01:
|
acl01:
|
||||||
terms:
|
terms:
|
||||||
@ -12,6 +23,14 @@ acls:
|
|||||||
source: 0.0.0.0/0
|
source: 0.0.0.0/0
|
||||||
destination: ::/0
|
destination: ::/0
|
||||||
action: permit
|
action: permit
|
||||||
|
- description: "Error, source prefixlist is IPv4 and destination prefixlist is IPv6"
|
||||||
|
source: v4only
|
||||||
|
destination: v6only
|
||||||
|
action: permit
|
||||||
|
- description: "Error, source prefixlist is IPv6 and destination is IPv4"
|
||||||
|
source: v6only
|
||||||
|
destination: 0.0.0.0/0
|
||||||
|
action: permit
|
||||||
- description: "Error, source is IPv6 and destination is IPv4"
|
- description: "Error, source is IPv6 and destination is IPv4"
|
||||||
source: ::/0
|
source: ::/0
|
||||||
destination: 192.168.0.1
|
destination: 192.168.0.1
|
||||||
@ -32,3 +51,13 @@ acls:
|
|||||||
source: 192.168.0.1
|
source: 192.168.0.1
|
||||||
destination: 10.0.0.0/8
|
destination: 10.0.0.0/8
|
||||||
action: permit
|
action: permit
|
||||||
|
- description: "OK"
|
||||||
|
source: v4only
|
||||||
|
action: permit
|
||||||
|
- description: "OK"
|
||||||
|
source: v6only
|
||||||
|
action: permit
|
||||||
|
- description: "OK"
|
||||||
|
source: v4only
|
||||||
|
destination: v4only
|
||||||
|
action: permit
|
||||||
|
Reference in New Issue
Block a user