summaryrefslogtreecommitdiff
path: root/lib/aclgenerator.py
diff options
context:
space:
mode:
authorJohan Lundberg <lundberg@nordu.net>2015-04-02 10:43:33 +0200
committerJohan Lundberg <lundberg@nordu.net>2015-04-02 10:43:33 +0200
commitbd611ac59f7c4db885a2f8631ef0bcdcd1901ca0 (patch)
treee60f5333a7699cd021b33c7f5292af55b774001b /lib/aclgenerator.py
Diffstat (limited to 'lib/aclgenerator.py')
-rwxr-xr-xlib/aclgenerator.py418
1 files changed, 418 insertions, 0 deletions
diff --git a/lib/aclgenerator.py b/lib/aclgenerator.py
new file mode 100755
index 0000000..c5be343
--- /dev/null
+++ b/lib/aclgenerator.py
@@ -0,0 +1,418 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""ACL Generator base class."""
+
+import copy
+import re
+from string import Template
+
+import policy
+
+
+# generic error class
+class Error(Exception):
+ """Base error class."""
+ pass
+
+
+class NoPlatformPolicyError(Error):
+ """Raised when a policy is received that doesn't support this platform."""
+ pass
+
+
+class UnsupportedFilter(Error):
+ """Raised when we see an inappropriate filter."""
+ pass
+
+
+class UnknownIcmpTypeError(Error):
+ """Raised when we see an unknown icmp-type."""
+ pass
+
+
+class MismatchIcmpInetError(Error):
+ """Raised when mistmatch between icmp/icmpv6 and inet/inet6."""
+ pass
+
+
+class EstablishedError(Error):
+ """Raised when a term has established option with inappropriate protocol."""
+ pass
+
+
+class UnsupportedAF(Error):
+ """Raised when provided an unsupported address family."""
+ pass
+
+
+class DuplicateTermError(Error):
+ """Raised when duplication of term names are detected."""
+ pass
+
+
+class UnsupportedFilterError(Error):
+ """Raised when we see an inappropriate filter."""
+ pass
+
+
+class TermNameTooLongError(Error):
+ """Raised when term named can not be abbreviated."""
+ pass
+
+
+class Term(object):
+ """Generic framework for a generator Term."""
+ ICMP_TYPE = policy.Term.ICMP_TYPE
+ PROTO_MAP = {'ip': 0,
+ 'icmp': 1,
+ 'igmp': 2,
+ 'ggp': 3,
+ 'ipencap': 4,
+ 'tcp': 6,
+ 'egp': 8,
+ 'igp': 9,
+ 'udp': 17,
+ 'rdp': 27,
+ 'ipv6': 41,
+ 'ipv6-route': 43,
+ 'ipv6-frag': 44,
+ 'rsvp': 46,
+ 'gre': 47,
+ 'esp': 50,
+ 'ah': 51,
+ 'icmpv6': 58,
+ 'ipv6-nonxt': 59,
+ 'ipv6-opts': 60,
+ 'ospf': 89,
+ 'ipip': 94,
+ 'pim': 103,
+ 'vrrp': 112,
+ 'l2tp': 115,
+ 'sctp': 132,
+ }
+ AF_MAP = {'inet': 4,
+ 'inet6': 6,
+ 'bridge': 4 # if this doesn't exist, output includes v4 & v6
+ }
+ # provide flipped key/value dicts
+ PROTO_MAP_BY_NUMBER = dict([(v, k) for (k, v) in PROTO_MAP.iteritems()])
+ AF_MAP_BY_NUMBER = dict([(v, k) for (k, v) in AF_MAP.iteritems()])
+
+ NO_AF_LOG_FORMAT = Template('Term $term will not be rendered, as it has'
+ ' $direction address match specified but no'
+ ' $direction addresses of $af address family'
+ ' are present.')
+
+ def NormalizeAddressFamily(self, af):
+ """Convert (if necessary) address family name to numeric value.
+
+ Args:
+ af: Address family, can be either numeric or string (e.g. 4 or 'inet')
+
+ Returns:
+ af: Numeric address family value
+
+ Raises:
+ UnsupportedAF: Address family not in keys or values of our AF_MAP.
+ """
+ # ensure address family (af) is valid
+ if af in self.AF_MAP_BY_NUMBER:
+ return af
+ elif af in self.AF_MAP:
+ # convert AF name to number (e.g. 'inet' becomes 4, 'inet6' becomes 6)
+ af = self.AF_MAP[af]
+ else:
+ raise UnsupportedAF('Address family %s is not supported, term %s.' % (
+ af, self.term.name))
+ return af
+
+ def NormalizeIcmpTypes(self, icmp_types, protocols, af):
+ """Return verified list of appropriate icmp-types.
+
+ Args:
+ icmp_types: list of icmp_types
+ protocols: list of protocols
+ af: address family of this term, either numeric or text (see self.AF_MAP)
+
+ Returns:
+ sorted list of numeric icmp-type codes.
+
+ Raises:
+ UnsupportedFilterError: icmp-types specified with non-icmp protocol.
+ MismatchIcmpInetError: mismatch between icmp protocol and address family.
+ UnknownIcmpTypeError: unknown icmp-type specified
+ """
+ if not icmp_types:
+ return ['']
+ # only protocols icmp or icmpv6 can be used with icmp-types
+ if protocols != ['icmp'] and protocols != ['icmpv6']:
+ raise UnsupportedFilterError('%s %s' % (
+ 'icmp-types specified for non-icmp protocols in term: ',
+ self.term.name))
+ # make sure we have a numeric address family (4 or 6)
+ af = self.NormalizeAddressFamily(af)
+ # check that addr family and protocl are appropriate
+ if ((af != 4 and protocols == ['icmp']) or
+ (af != 6 and protocols == ['icmpv6'])):
+ raise MismatchIcmpInetError('%s %s' % (
+ 'ICMP/ICMPv6 mismatch with address family IPv4/IPv6 in term',
+ self.term.name))
+ # ensure all icmp types are valid
+ for icmptype in icmp_types:
+ if icmptype not in self.ICMP_TYPE[af]:
+ raise UnknownIcmpTypeError('%s %s %s %s' % (
+ '\nUnrecognized ICMP-type (', icmptype,
+ ') specified in term ', self.term.name))
+ rval = []
+ rval.extend([self.ICMP_TYPE[af][x] for x in icmp_types])
+ rval.sort()
+ return rval
+
+
+class ACLGenerator(object):
+ """Generates platform specific filters and terms from a policy object.
+
+ This class takes a policy object and renders the output into a syntax which
+ is understood by a specific platform (eg. iptables, cisco, etc).
+ """
+
+ _PLATFORM = None
+ # Default protocol to apply when no protocol is specified.
+ _DEFAULT_PROTOCOL = 'ip'
+ # Unsupported protocols by address family.
+ _SUPPORTED_AF = set(('inet', 'inet6'))
+ # Commonly misspelled protocols that the generator should reject.
+ _FILTER_BLACKLIST = {}
+
+ # Set of required keywords that every generator must support.
+ _REQUIRED_KEYWORDS = set(['action',
+ 'comment',
+ 'destination_address',
+ 'destination_address_exclude',
+ 'destination_port',
+ 'icmp_type',
+ 'name', # obj attribute, not keyword
+ 'option',
+ 'protocol',
+ 'platform',
+ 'platform_exclude',
+ 'source_address',
+ 'source_address_exclude',
+ 'source_port',
+ 'translated', # obj attribute, not keyword
+ 'verbatim',
+ ])
+ # Generators should redefine this in subclass as optional support is added
+ _OPTIONAL_SUPPORTED_KEYWORDS = set([])
+
+ # Abbreviation table used to automatically abbreviate terms that exceed
+ # specified limit. We use uppercase for abbreviations to distinguish
+ # from lowercase names. This is order list - we try the ones in the
+ # top of the list before the ones later in the list. Prefer clear
+ # or very-space-saving abbreviations by putting them early in the
+ # list. Abbreviations may be regular expressions or fixed terms;
+ # prefer fixed terms unless there's a clear benefit to regular
+ # expressions.
+ _ABBREVIATION_TABLE = [
+ ('bogons', 'BGN'),
+ ('bogon', 'BGN'),
+ ('reserved', 'RSV'),
+ ('rfc1918', 'PRV'),
+ ('rfc-1918', 'PRV'),
+ ('internet', 'EXT'),
+ ('global', 'GBL'),
+ ('internal', 'INT'),
+ ('customer', 'CUST'),
+ ('google', 'GOOG'),
+ ('ballmer', 'ASS'),
+ ('microsoft', 'LOL'),
+ ('china', 'BAN'),
+ ('border', 'BDR'),
+ ('service', 'SVC'),
+ ('router', 'RTR'),
+ ('transit', 'TRNS'),
+ ('experiment', 'EXP'),
+ ('established', 'EST'),
+ ('unreachable', 'UNR'),
+ ('fragment', 'FRG'),
+ ('accept', 'OK'),
+ ('discard', 'DSC'),
+ ('reject', 'REJ'),
+ ('replies', 'ACK'),
+ ('request', 'REQ'),
+ ]
+ # Maximum term length. Can be overriden by generator to enforce
+ # platform specific restrictions.
+ _TERM_MAX_LENGTH = 62
+
+ def __init__(self, pol, exp_info):
+ """Initialise an ACLGenerator. Store policy structure for processing."""
+ object.__init__(self)
+
+ # The default list of valid keyword tokens for generators
+ self._VALID_KEYWORDS = self._REQUIRED_KEYWORDS.union(
+ self._OPTIONAL_SUPPORTED_KEYWORDS)
+
+ self.policy = pol
+
+ for header, terms in pol.filters:
+ if self._PLATFORM in header.platforms:
+ # Verify valid keywords
+ # error on unsupported optional keywords that could result
+ # in dangerous or unexpected results
+ for term in terms:
+ # Only verify optional keywords if the term is active on the platform.
+ err = []
+ if term.platform:
+ if self._PLATFORM not in term.platform:
+ continue
+ if term.platform_exclude:
+ if self._PLATFORM in term.platform_exclude:
+ continue
+ for el, val in term.__dict__.items():
+ # Private attributes do not need to be valid keywords.
+ if (val and el not in self._VALID_KEYWORDS
+ and not el.startswith('flatten')):
+ err.append(el)
+ if err:
+ raise UnsupportedFilterError('%s %s %s %s %s %s' % ('\n', term.name,
+ 'unsupported optional keywords for target', self._PLATFORM,
+ 'in policy:', ' '.join(err)))
+ continue
+
+ self._TranslatePolicy(pol, exp_info)
+
+ def _TranslatePolicy(self, pol, exp_info):
+ """Translate policy contents to platform specific data structures."""
+ raise Error('%s does not implement _TranslatePolicies()' % self._PLATFORM)
+
+ def FixHighPorts(self, term, af='inet', all_protocols_stateful=False):
+ """Evaluate protocol and ports of term, return sane version of term."""
+ mod = term
+
+ # Determine which protocols this term applies to.
+ if term.protocol:
+ protocols = set(term.protocol)
+ else:
+ protocols = set((self._DEFAULT_PROTOCOL,))
+
+ # Check that the address family matches the protocols.
+ if not af in self._SUPPORTED_AF:
+ raise UnsupportedAF('\nAddress family %s, found in %s, '
+ 'unsupported by %s' % (af, term.name, self._PLATFORM))
+ if af in self._FILTER_BLACKLIST:
+ unsupported_protocols = self._FILTER_BLACKLIST[af].intersection(protocols)
+ if unsupported_protocols:
+ raise UnsupportedFilter('\n%s targets do not support protocol(s) %s '
+ 'with address family %s (in %s)' %
+ (self._PLATFORM, unsupported_protocols,
+ af, term.name))
+
+ # Many renders expect high ports for terms with the established option.
+ for opt in [str(x) for x in term.option]:
+ if opt.find('established') == 0:
+ unstateful_protocols = protocols.difference(set(('tcp', 'udp')))
+ if not unstateful_protocols:
+ # TCP/UDP: add in high ports then collapse to eliminate overlaps.
+ mod = copy.deepcopy(term)
+ mod.destination_port.append((1024, 65535))
+ mod.destination_port = mod.CollapsePortList(mod.destination_port)
+ elif not all_protocols_stateful:
+ errmsg = 'Established option supplied with inappropriate protocol(s)'
+ raise EstablishedError('%s %s %s %s' %
+ (errmsg, unstateful_protocols,
+ 'in term', term.name))
+ break
+
+ return mod
+
+ def FixTermLength(self, term_name, abbreviate=False, truncate=False):
+ """Return a term name which is equal or shorter than _TERM_MAX_LENGTH.
+
+ New term is obtained in two steps. First, if allowed, automatic
+ abbreviation is performed using hardcoded abbreviation table. Second,
+ if allowed, term name is truncated to specified limit.
+
+ Args:
+ term_name: Name to abbreviate if necessary.
+ abbreviate: Whether to allow abbreviations to shorten the length.
+ truncate: Whether to allow truncation to shorten the length.
+ Returns:
+ A string based on term_name, that is equal or shorter than
+ _TERM_MAX_LENGTH abbreviated and truncated as necessary.
+ Raises:
+ TermNameTooLongError: term_name cannot be abbreviated
+ to be shorter than _TERM_MAX_LENGTH, or truncation is disabled.
+ """
+ new_term = term_name
+ if abbreviate:
+ for word, abbrev in self._ABBREVIATION_TABLE:
+ if len(new_term) <= self._TERM_MAX_LENGTH:
+ return new_term
+ new_term = re.sub(word, abbrev, new_term)
+ if truncate:
+ new_term = new_term[:self._TERM_MAX_LENGTH]
+ if len(new_term) <= self._TERM_MAX_LENGTH:
+ return new_term
+ raise TermNameTooLongError('Term %s (originally %s) is '
+ 'too long. Limit is %d characters (vs. %d) '
+ 'and no abbreviations remain or abbreviations '
+ 'disabled.' %
+ (new_term, term_name,
+ self._TERM_MAX_LENGTH,
+ len(new_term)))
+
+
+def AddRepositoryTags(prefix=''):
+ """Add repository tagging into the output.
+
+ Args:
+ prefix: comment delimiter, if needed, to appear before tags
+ Returns:
+ list of text lines containing revision data
+ """
+ tags = []
+ p4_id = '%sId:%s' % ('$', '$')
+ p4_date = '%sDate:%s' % ('$', '$')
+ tags.append('%s%s' % (prefix, p4_id))
+ tags.append('%s%s' % (prefix, p4_date))
+ return tags
+
+
+def WrapWords(textlist, size, joiner='\n'):
+ """Insert breaks into the listed strings at specified width.
+
+ Args:
+ textlist: a list of text strings
+ size: width of reformated strings
+ joiner: text to insert at break. eg. '\n ' to add an indent.
+ Returns:
+ list of strings
+ """
+ # \S*? is a non greedy match to collect words of len > size
+ # .{1,%d} collects words and spaces up to size in length.
+ # (?:\s|\Z) ensures that we break on spaces or at end of string.
+ rval = []
+ linelength_re = re.compile(r'(\S*?.{1,%d}(?:\s|\Z))' % size)
+ for index in range(len(textlist)):
+ if len(textlist[index]) > size:
+ # insert joiner into the string at appropriate places.
+ textlist[index] = joiner.join(linelength_re.findall(textlist[index]))
+ # avoid empty comment lines
+ rval.extend(x.strip() for x in textlist[index].strip().split(joiner) if x)
+ return rval