Skip to content

Commit 0b36b02

Browse files
abhindesCapirca Team
authored andcommitted
This adds inet6 filter_option support to gce generator.
PiperOrigin-RevId: 378303424
1 parent d067ee0 commit 0b36b02

5 files changed

Lines changed: 602 additions & 20 deletions

File tree

capirca/lib/gce.py

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from typing import Dict, Text, Any
3636

37-
from capirca.lib import aclgenerator
37+
from capirca.lib import gcp
3838
from capirca.lib import nacaddr
3939
import six
4040
from six.moves import range
@@ -74,13 +74,18 @@ def IsDefaultDeny(term):
7474
return True
7575

7676

77-
class Term(aclgenerator.Term):
77+
class Term(gcp.Term):
7878
"""Creates the term for the GCE firewall."""
7979

8080
ACTION_MAP = {'accept': 'allowed',
8181
'deny': 'denied'}
82-
# Restrict the number of terms to 256. Proto supports up to 256
82+
# Restrict the number of addresses per term to 256.
83+
# Similar restrictions apply to source and target tags, and ports.
84+
# Details: https://cloud.google.com/vpc/docs/quota#per_network_2
8385
_TERM_ADDRESS_LIMIT = 256
86+
_TERM_SOURCE_TAGS_LIMIT = 30
87+
_TERM_TARGET_TAGS_LIMIT = 70
88+
_TERM_PORTS_LIMIT = 256
8489

8590
# Firewall rule name has to match specific RE:
8691
# The first character must be a lowercase letter, and all following characters
@@ -97,11 +102,12 @@ class Term(aclgenerator.Term):
97102
])
98103

99104
# Any protocol not in _ALLOW_PROTO_NAME must be passed by number.
100-
ALWAYS_PROTO_NUM = set(aclgenerator.Term.PROTO_MAP.keys()) - _ALLOW_PROTO_NAME
105+
ALWAYS_PROTO_NUM = set(gcp.Term.PROTO_MAP.keys()) - _ALLOW_PROTO_NAME
101106

102-
def __init__(self, term):
107+
def __init__(self, term, inet_version='inet'):
103108
super(Term, self).__init__(term)
104109
self.term = term
110+
self.inet_version = inet_version
105111

106112
self._validateDirection()
107113
if self.term.source_address_exclude and not self.term.source_address:
@@ -189,6 +195,16 @@ def ConvertToDict(self):
189195
term_dict['name'] = '%s-%s' % (
190196
self.term.network.split('/')[-1], term_dict['name'])
191197

198+
# Checking counts of tags, and ports to see if they exceeded limits.
199+
if len(self.term.source_tag) > self._TERM_SOURCE_TAGS_LIMIT:
200+
raise GceFirewallError(
201+
'GCE firewall rule exceeded number of source tags per rule: %s' %
202+
self.term.name)
203+
if len(self.term.destination_tag) > self._TERM_TARGET_TAGS_LIMIT:
204+
raise GceFirewallError(
205+
'GCE firewall rule exceeded number of target tags per rule: %s' %
206+
self.term.name)
207+
192208
if self.term.source_tag:
193209
if self.term.direction == 'INGRESS':
194210
term_dict['sourceTags'] = self.term.source_tag
@@ -200,10 +216,29 @@ def ConvertToDict(self):
200216
term_dict['priority'] = self.term.priority
201217

202218
rules = []
203-
saddrs = sorted(self.term.GetAddressOfVersion('source_address', 4),
204-
key=ipaddress.get_mixed_type_key)
205-
daddrs = sorted(self.term.GetAddressOfVersion('destination_address', 4),
219+
# TODO(abhindes) correctly account for 'mixed' as well
220+
term_af = self.AF_MAP.get(self.inet_version)
221+
saddrs = sorted(self.term.GetAddressOfVersion('source_address', term_af),
206222
key=ipaddress.get_mixed_type_key)
223+
daddrs = sorted(
224+
self.term.GetAddressOfVersion('destination_address', term_af),
225+
key=ipaddress.get_mixed_type_key)
226+
227+
# If the address got filtered out and is empty due to address family, we
228+
# don't render the term. At this point of term processing, the direction has
229+
# already been validated, so we can just log and return empty rule.
230+
if self.term.source_address and not saddrs:
231+
logging.warning(
232+
'WARNING: Term %s is not being rendered for %s, '
233+
'because there are no addresses of that family.', self.term.name,
234+
self.inet_version)
235+
return []
236+
if self.term.destination_address and not daddrs:
237+
logging.warning(
238+
'WARNING: Term %s is not being rendered for %s, '
239+
'because there are no addresses of that family.', self.term.name,
240+
self.inet_version)
241+
return []
207242

208243
if not self.term.protocol:
209244
raise GceFirewallError(
@@ -214,18 +249,59 @@ def ConvertToDict(self):
214249
if self.term.logging:
215250
proto_dict['logConfig'] = {'enable': True}
216251

252+
filtered_protocols = []
217253
for proto in self.term.protocol:
254+
# ICMP filtering by inet_version
255+
# TODO(abhindes) deal with "mixed" correctly
256+
# Convert protocol to number for uniformity of comparison.
257+
# PROTO_MAP always returns protocol number.
258+
if proto in self._ALLOW_PROTO_NAME:
259+
proto_num = self.PROTO_MAP[proto]
260+
else:
261+
proto_num = proto
262+
if proto_num == self.PROTO_MAP['icmp'] and self.inet_version == 'inet6':
263+
logging.warning(
264+
'WARNING: Term %s is being rendered for inet6, ICMP '
265+
'protocol will not be rendered.', self.term.name)
266+
continue
267+
if proto_num == self.PROTO_MAP['icmpv6'] and self.inet_version == 'inet':
268+
logging.warning(
269+
'WARNING: Term %s is being rendered for inet, ICMPv6 '
270+
'protocol will not be rendered.', self.term.name)
271+
continue
272+
if proto_num == self.PROTO_MAP['igmp'] and self.inet_version == 'inet6':
273+
logging.warning(
274+
'WARNING: Term %s is being rendered for inet6, IGMP '
275+
'protocol will not be rendered.', self.term.name)
276+
continue
277+
filtered_protocols.append(proto)
278+
# If there is no protocol left after ICMP/IGMP filtering, drop this term.
279+
if not filtered_protocols:
280+
return []
281+
for proto in filtered_protocols:
282+
# If the protocol name is not supported, protocol number is used.
283+
# This is done by default in policy.py.
284+
if proto not in self._ALLOW_PROTO_NAME:
285+
logging.info(
286+
'INFO: Term %s is being rendered using protocol number',
287+
self.term.name)
218288
dest = {
219289
'IPProtocol': proto
220290
}
221291

222292
if self.term.destination_port:
223-
ports = dest.setdefault('ports', [])
293+
ports = []
224294
for start, end in self.term.destination_port:
225295
if start == end:
226296
ports.append(str(start))
227297
else:
228298
ports.append('%d-%d' % (start, end))
299+
if len(ports) > self._TERM_PORTS_LIMIT:
300+
raise GceFirewallError(
301+
'GCE firewall rule exceeded number of ports per rule: %s' %
302+
self.term.name)
303+
dest['ports'] = ports
304+
229305
action = self.ACTION_MAP[self.term.action[0]]
230306
dict_val = []
231307
if action in proto_dict:
@@ -268,12 +344,16 @@ def ConvertToDict(self):
268344
return rules
269345

270346

271-
class GCE(aclgenerator.ACLGenerator):
347+
class GCE(gcp.GCP):
272348
"""A GCE firewall policy object."""
273349

274350
_PLATFORM = 'gce'
275351
SUFFIX = '.gce'
276-
_SUPPORTED_AF = set(('inet'))
352+
_SUPPORTED_AF = frozenset(('inet', 'inet6'))
353+
_ANY_IP = {
354+
'inet': nacaddr.IP('0.0.0.0/0'),
355+
'inet6': nacaddr.IP('::/0'),
356+
}
277357
# Supported is 63 but we need to account for dynamic updates when the term
278358
# is rendered (which can add proto and a counter).
279359
_TERM_MAX_LENGTH = 53
@@ -330,6 +410,12 @@ def _TranslatePolicy(self, pol, exp_info):
330410
if i in filter_options:
331411
direction = i
332412
filter_options.remove(i)
413+
# Get the address family if set.
414+
address_family = 'inet'
415+
for i in self._SUPPORTED_AF:
416+
if i in filter_options:
417+
address_family = i
418+
filter_options.remove(i)
333419

334420
for opt in filter_options:
335421
try:
@@ -351,13 +437,11 @@ def _TranslatePolicy(self, pol, exp_info):
351437
terms[-1].protocol = ['all']
352438
terms[-1].priority = 65534
353439
if direction == 'EGRESS':
354-
terms[-1].destination_address = [nacaddr.IP('0.0.0.0/0'),
355-
nacaddr.IP('::/0')]
440+
if address_family != 'mixed':
441+
terms[-1].destination_address = [self._ANY_IP[address_family]]
356442
else:
357-
terms[-1].source_address = [
358-
nacaddr.IP('0.0.0.0/0'),
359-
nacaddr.IP('::/0')
360-
]
443+
if address_family != 'mixed':
444+
terms[-1].source_address = [self._ANY_IP[address_family]]
361445

362446
for term in terms:
363447
if term.stateless_reply:
@@ -388,7 +472,7 @@ def _TranslatePolicy(self, pol, exp_info):
388472
raise GceFirewallError(
389473
'GCE firewall does not support term options.')
390474

391-
for rules in Term(term).ConvertToDict():
475+
for rules in Term(term, address_family).ConvertToDict():
392476
logging.debug('Attribute count of rule %s is: %d', term.name,
393477
GetAttributeCount(rules))
394478
total_attribute_count += GetAttributeCount(rules)

def/NETWORK.net

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ RESERVED = 0.0.0.0/8 # reserved
5151

5252
ANY = 0.0.0.0/0
5353

54+
ANY_V6 = ::/0
55+
56+
ANY_MIXED = ANY
57+
ANY_V6
58+
5459
# http://www.team-cymru.org/Services/Bogons/bogon-bn-agg.txt
5560
# 22-Apr-2011
5661
BOGON = 0.0.0.0/8

policies/pol/sample_gce.pol

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
header {
66
comment:: "this is a sample policy to generate GCE filter"
7-
target:: gce global/networks/default
7+
target:: gce global/networks/default inet
88
}
99

1010
term test-ssh {
@@ -31,20 +31,60 @@ term test-icmp {
3131
action:: accept
3232
}
3333

34+
term test-icmpv6 {
35+
comment:: "Allow ICMPv6 from company. This should not be rendered."
36+
source-address:: PUBLIC_NAT
37+
protocol:: icmpv6
38+
action:: accept
39+
}
40+
41+
term test-igmp {
42+
comment:: "Allow IGMP from company."
43+
source-address:: PUBLIC_NAT
44+
protocol:: igmp
45+
action:: accept
46+
}
47+
48+
term test-multiple-protocols {
49+
comment:: "Allow TCP/UDP access to all instances from company."
50+
source-address:: PUBLIC_NAT
51+
protocol:: tcp udp
52+
destination-port:: HIGH_PORTS
53+
action:: accept
54+
}
55+
56+
term test-multiple-protocols-tcp-icmpv6 {
57+
comment:: "Allow all tcp and icmpv6. This should only render tcp."
58+
source-address:: PUBLIC_NAT
59+
protocol:: tcp icmpv6
60+
action:: accept
61+
}
62+
63+
term test-multiple-protocols-tcp-icmp {
64+
comment:: "Allow all tcp and icmp."
65+
source-address:: PUBLIC_NAT
66+
protocol:: tcp icmp
67+
action:: accept
68+
}
69+
3470
term test-internal {
3571
comment:: "Allow all GCE network internal traffic."
3672
source-address:: RFC1918
3773
protocol:: tcp udp
3874
action:: accept
3975
}
4076

77+
term default-deny {
78+
action:: deny
79+
}
80+
4181
#
4282
# Sample EGRESS policy
4383
# If source-tag is included, it maps to targetTags in the GCP Egress rule
4484
#
4585
header {
4686
comment:: "this is a sample policy to generate EGRESS GCE filter"
47-
target:: gce EGRESS global/networks/default
87+
target:: gce EGRESS global/networks/default inet
4888
}
4989

5090
term test-egress-address {
@@ -64,4 +104,14 @@ term test-egress-tag {
64104
action:: accept
65105
}
66106

107+
term test-egress-address-v6-only {
108+
comment:: "Outbound to IPv6 Server. This should not be rendered."
109+
protocol:: tcp
110+
destination-port:: SMTP
111+
destination-address:: PUBLIC_IPV6_SERVERS
112+
action:: accept
113+
}
67114

115+
term default-deny {
116+
action:: deny
117+
}

0 commit comments

Comments
 (0)