3434
3535from typing import Dict , Text , Any
3636
37- from capirca .lib import aclgenerator
37+ from capirca .lib import gcp
3838from capirca .lib import nacaddr
3939import six
4040from 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 )
0 commit comments