#!/usr/bin/python
#
# Copyright 2013 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.
#

"""Ipset iptables generator.  This is a subclass of Iptables generator.

ipset is a system inside the Linux kernel, which can very efficiently store
and match IPv4 and IPv6 addresses. This can be used to dramatically increase
performace of iptables firewall.

"""

__author__ = '<EMAIL> (<NAME>)'

from string import Template

import iptables
import nacaddr


class Error(Exception):
  pass


class Term(iptables.Term):
  """Single Ipset term representation."""

  _PLATFORM = 'ipset'
  _SET_MAX_LENGTH = 31
  _POSTJUMP_FORMAT = None
  _PREJUMP_FORMAT = None
  _TERM_FORMAT = None
  _COMMENT_FORMAT = Template('-A $filter -m comment --comment "$comment"')
  _FILTER_TOP_FORMAT = Template('-A $filter')

  def __init__(self, *args, **kwargs):
    super(Term, self).__init__(*args, **kwargs)
    # This stores tuples of set name and set contents, keyed by direction.
    # For example:
    # { 'src': ('term_name', [ipaddr object, ipaddr object]),
    #  'dst': ('term_name', [ipaddr object, ipaddr object]) }
    self.addr_sets = dict()

  def _CalculateAddresses(self, src_addr_list, src_ex_addr_list,
                          dst_addr_list, dst_ex_addr_list):
    """Calculate source and destination address list for a term.

    Since ipset is very efficient at matching large number of
    addresses, we never return eny exclude addresses. Instead
    least positive match is calculated for both source and destination
    addresses.

    For source and destination address list, three cases are possible.
    First case is when there is no addresses. In that case we return
    _all_ips.
    Second case is when there is strictly one address. In that case,
    we optimize by not generating a set, and it's then the only
    element of returned set.
    Third case case is when there is more than one address in a set.
    In that case we generate a set and also return _all_ips. Note the
    difference to the first case where no set is actually generated.

    Args:
      src_addr_list: source address list of the term.
      src_ex_addr_list: source address exclude list of the term.
      dst_addr_list: destination address list of the term.
      dst_ex_addr_list: destination address exclude list of the term.

    Returns:
      tuple containing source address list, source exclude address list,
      destination address list, destination exclude address list in
      that order.

    """
    if not src_addr_list:
      src_addr_list = [self._all_ips]
    src_addr_list = [src_addr for src_addr in src_addr_list if
                     src_addr.version == self.AF_MAP[self.af]]
    if src_ex_addr_list:
      src_ex_addr_list = [src_ex_addr for src_ex_addr in src_ex_addr_list if
                          src_ex_addr.version == self.AF_MAP[self.af]]
      src_addr_list = nacaddr.ExcludeAddrs(src_addr_list, src_ex_addr_list)
    if len(src_addr_list) > 1:
      set_name = self._GenerateSetName(self.term.name, 'src')
      self.addr_sets['src'] = (set_name, src_addr_list)
      src_addr_list = [self._all_ips]

    if not dst_addr_list:
      dst_addr_list = [self._all_ips]
    dst_addr_list = [dst_addr for dst_addr in dst_addr_list if
                     dst_addr.version == self.AF_MAP[self.af]]
    if dst_ex_addr_list:
      dst_ex_addr_list = [dst_ex_addr for dst_ex_addr in dst_ex_addr_list if
                          dst_ex_addr.version == self.AF_MAP[self.af]]
      dst_addr_list = nacaddr.ExcludeAddrs(dst_addr_list, dst_ex_addr_list)
    if len(dst_addr_list) > 1:
      set_name = self._GenerateSetName(self.term.name, 'dst')
      self.addr_sets['dst'] = (set_name, dst_addr_list)
      dst_addr_list = [self._all_ips]
    return (src_addr_list, [], dst_addr_list, [])

  def _GenerateAddressStatement(self, src_addr, dst_addr):
    """Return the address section of an individual iptables rule.

    See _CalculateAddresses documentation. Three cases are possible here,
    and they map directly to cases in _CalculateAddresses.
    First, there can be no addresses for a direction (value is _all_ips then)
    In that case we return empty string.
    Second there can be stricly one address. In that case we return single
    address match (-s or -d).
    Third case, is when the value is _all_ips but also the set for particular
    direction is present. That's when we return a set match.

    Args:
      src_addr: source address of the rule.
      dst_addr: destination address of the rule.

    Returns:
      tuple containing source and destination address statement, in
      that order.

    """
    src_addr_stmt = ''
    dst_addr_stmt = ''
    if src_addr and dst_addr:
      if src_addr == self._all_ips:
        if 'src' in self.addr_sets:
          src_addr_stmt = ('-m set --set %s src' % self.addr_sets['src'][0])
      else:
        src_addr_stmt = '-s %s/%d' % (src_addr.ip, src_addr.prefixlen)
      if dst_addr == self._all_ips:
        if 'dst' in self.addr_sets:
          dst_addr_stmt = ('-m set --set %s dst' % self.addr_sets['dst'][0])
      else:
        dst_addr_stmt = '-d %s/%d' % (dst_addr.ip, dst_addr.prefixlen)
    return (src_addr_stmt, dst_addr_stmt)

  def _GenerateSetName(self, term_name, suffix):
    if self.af == 'inet6':
      suffix += '-v6'
    if len(term_name) + len(suffix) + 1 > self._SET_MAX_LENGTH:
      term_name = term_name[:self._SET_MAX_LENGTH -
                            (len(term_name) + len(suffix) + 1)]
    return term_name + '-' + suffix


class Ipset(iptables.Iptables):
  """Ipset generator."""
  _PLATFORM = 'ipset'
  _SET_TYPE = 'hash:net'
  _SUFFIX = '.ips'
  _TERM = Term

  def __str__(self):
    # Actual rendering happens in __str__, so it has to be called
    # before we do set specific part.
    iptables_output = iptables.Iptables.__str__(self)
    output = []
    for (_, _, _, _, terms) in self.iptables_policies:
      for term in terms:
        output.extend(self._GenerateSetConfig(term))
    output.append(iptables_output)
    return '\n'.join(output)

  def _GenerateSetConfig(self, term):
    """Generate set configuration for supplied term.

    Args:
      term: input term.

    Returns:
      string that is configuration of supplied term.

    """
    output = []
    for direction in sorted(term.addr_sets, reverse=True):
      set_hashsize = 2 ** len(term.addr_sets[direction][1]).bit_length()
      set_maxelem = 2 ** len(term.addr_sets[direction][1]).bit_length()
      output.append('create %s %s family %s hashsize %i maxelem %i' %
                    (term.addr_sets[direction][0],
                     self._SET_TYPE,
                     term.af,
                     set_hashsize,
                     set_maxelem))
      for address in term.addr_sets[direction][1]:
        output.append('add %s %s' % (term.addr_sets[direction][0], address))
    return output

#!/usr/bin/python
#
# Copyright 2013 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.
#

"""Ipset iptables generator.  This is a subclass of Iptables generator.

ipset is a system inside the Linux kernel, which can very efficiently store
and match IPv4 and IPv6 addresses. This can be used to dramatically increase
performace of iptables firewall.

"""

__author__ = '<EMAIL> (<NAME>)'

from string import Template

import iptables
import nacaddr


class Error(Exception):
  pass


class Term(iptables.Term):
  """Single Ipset term representation."""

  _PLATFORM = 'ipset'
  _SET_MAX_LENGTH = 31
  _POSTJUMP_FORMAT = None
  _PREJUMP_FORMAT = None
  _TERM_FORMAT = None
  _COMMENT_FORMAT = Template('-A $filter -m comment --comment "$comment"')
  _FILTER_TOP_FORMAT = Template('-A $filter')

  def __init__(self, *args, **kwargs):
    super(Term, self).__init__(*args, **kwargs)
    # This stores tuples of set name and set contents, keyed by direction.
    # For example:
    # { 'src': ('term_name', [ipaddr object, ipaddr object]),
    #  'dst': ('term_name', [ipaddr object, ipaddr object]) }
    self.addr_sets = dict()

  def _CalculateAddresses(self, src_addr_list, src_ex_addr_list,
                          dst_addr_list, dst_ex_addr_list):
    """Calculate source and destination address list for a term.

    Since ipset is very efficient at matching large number of
    addresses, we never return eny exclude addresses. Instead
    least positive match is calculated for both source and destination
    addresses.

    For source and destination address list, three cases are possible.
    First case is when there is no addresses. In that case we return
    _all_ips.
    Second case is when there is strictly one address. In that case,
    we optimize by not generating a set, and it's then the only
    element of returned set.
    Third case case is when there is more than one address in a set.
    In that case we generate a set and also return _all_ips. Note the
    difference to the first case where no set is actually generated.

    Args:
      src_addr_list: source address list of the term.
      src_ex_addr_list: source address exclude list of the term.
      dst_addr_list: destination address list of the term.
      dst_ex_addr_list: destination address exclude list of the term.

    Returns:
      tuple containing source address list, source exclude address list,
      destination address list, destination exclude address list in
      that order.

    """
    if not src_addr_list:
      src_addr_list = [self._all_ips]
    src_addr_list = [src_addr for src_addr in src_addr_list if
                     src_addr.version == self.AF_MAP[self.af]]
    if src_ex_addr_list:
      src_ex_addr_list = [src_ex_addr for src_ex_addr in src_ex_addr_list if
                          src_ex_addr.version == self.AF_MAP[self.af]]
      src_addr_list = nacaddr.ExcludeAddrs(src_addr_list, src_ex_addr_list)
    if len(src_addr_list) > 1:
      set_name = self._GenerateSetName(self.term.name, 'src')
      self.addr_sets['src'] = (set_name, src_addr_list)
      src_addr_list = [self._all_ips]

    if not dst_addr_list:
      dst_addr_list = [self._all_ips]
    dst_addr_list = [dst_addr for dst_addr in dst_addr_list if
                     dst_addr.version == self.AF_MAP[self.af]]
    if dst_ex_addr_list:
      dst_ex_addr_list = [dst_ex_addr for dst_ex_addr in dst_ex_addr_list if
                          dst_ex_addr.version == self.AF_MAP[self.af]]
      dst_addr_list = nacaddr.ExcludeAddrs(dst_addr_list, dst_ex_addr_list)
    if len(dst_addr_list) > 1:
      set_name = self._GenerateSetName(self.term.name, 'dst')
      self.addr_sets['dst'] = (set_name, dst_addr_list)
      dst_addr_list = [self._all_ips]
    return (src_addr_list, [], dst_addr_list, [])

  def _GenerateAddressStatement(self, src_addr, dst_addr):
    """Return the address section of an individual iptables rule.

    See _CalculateAddresses documentation. Three cases are possible here,
    and they map directly to cases in _CalculateAddresses.
    First, there can be no addresses for a direction (value is _all_ips then)
    In that case we return empty string.
    Second there can be stricly one address. In that case we return single
    address match (-s or -d).
    Third case, is when the value is _all_ips but also the set for particular
    direction is present. That's when we return a set match.

    Args:
      src_addr: source address of the rule.
      dst_addr: destination address of the rule.

    Returns:
      tuple containing source and destination address statement, in
      that order.

    """
    src_addr_stmt = ''
    dst_addr_stmt = ''
    if src_addr and dst_addr:
      if src_addr == self._all_ips:
        if 'src' in self.addr_sets:
          src_addr_stmt = ('-m set --set %s src' % self.addr_sets['src'][0])
      else:
        src_addr_stmt = '-s %s/%d' % (src_addr.ip, src_addr.prefixlen)
      if dst_addr == self._all_ips:
        if 'dst' in self.addr_sets:
          dst_addr_stmt = ('-m set --set %s dst' % self.addr_sets['dst'][0])
      else:
        dst_addr_stmt = '-d %s/%d' % (dst_addr.ip, dst_addr.prefixlen)
    return (src_addr_stmt, dst_addr_stmt)

  def _GenerateSetName(self, term_name, suffix):
    if self.af == 'inet6':
      suffix += '-v6'
    if len(term_name) + len(suffix) + 1 > self._SET_MAX_LENGTH:
      term_name = term_name[:self._SET_MAX_LENGTH -
                            (len(term_name) + len(suffix) + 1)]
    return term_name + '-' + suffix


class Ipset(iptables.Iptables):
  """Ipset generator."""
  _PLATFORM = 'ipset'
  _SET_TYPE = 'hash:net'
  _SUFFIX = '.ips'
  _TERM = Term

  def __str__(self):
    # Actual rendering happens in __str__, so it has to be called
    # before we do set specific part.
    iptables_output = iptables.Iptables.__str__(self)
    output = []
    for (_, _, _, _, terms) in self.iptables_policies:
      for term in terms:
        output.extend(self._GenerateSetConfig(term))
    output.append(iptables_output)
    return '\n'.join(output)

  def _GenerateSetConfig(self, term):
    """Generate set configuration for supplied term.

    Args:
      term: input term.

    Returns:
      string that is configuration of supplied term.

    """
    output = []
    for direction in sorted(term.addr_sets, reverse=True):
      set_hashsize = 2 ** len(term.addr_sets[direction][1]).bit_length()
      set_maxelem = 2 ** len(term.addr_sets[direction][1]).bit_length()
      output.append('create %s %s family %s hashsize %i maxelem %i' %
                    (term.addr_sets[direction][0],
                     self._SET_TYPE,
                     term.af,
                     set_hashsize,
                     set_maxelem))
      for address in term.addr_sets[direction][1]:
        output.append('add %s %s' % (term.addr_sets[direction][0], address))
    return output

#!/usr/bin/python # # Copyright 2013 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. # Ipset iptables generator.  This is a subclass of Iptables generator.  ipset is a system inside the Linux kernel, which can very efficiently store and match IPv4 and IPv6 addresses. This can be used to dramatically increase performace of iptables firewall. Single Ipset term representation. # This stores tuples of set name and set contents, keyed by direction. # For example: # { 'src': ('term_name', [ipaddr object, ipaddr object]), #  'dst': ('term_name', [ipaddr object, ipaddr object]) } Calculate source and destination address list for a term.      Since ipset is very efficient at matching large number of     addresses, we never return eny exclude addresses. Instead     least positive match is calculated for both source and destination     addresses.      For source and destination address list, three cases are possible.     First case is when there is no addresses. In that case we return     _all_ips.     Second case is when there is strictly one address. In that case,     we optimize by not generating a set, and it's then the only     element of returned set.     Third case case is when there is more than one address in a set.     In that case we generate a set and also return _all_ips. Note the     difference to the first case where no set is actually generated.      Args:       src_addr_list: source address list of the term.       src_ex_addr_list: source address exclude list of the term.       dst_addr_list: destination address list of the term.       dst_ex_addr_list: destination address exclude list of the term.      Returns:       tuple containing source address list, source exclude address list,       destination address list, destination exclude address list in       that order. Return the address section of an individual iptables rule.      See _CalculateAddresses documentation. Three cases are possible here,     and they map directly to cases in _CalculateAddresses.     First, there can be no addresses for a direction (value is _all_ips then)     In that case we return empty string.     Second there can be stricly one address. In that case we return single     address match (-s or -d).     Third case, is when the value is _all_ips but also the set for particular     direction is present. That's when we return a set match.      Args:       src_addr: source address of the rule.       dst_addr: destination address of the rule.      Returns:       tuple containing source and destination address statement, in       that order. Ipset generator. # Actual rendering happens in __str__, so it has to be called # before we do set specific part. Generate set configuration for supplied term.      Args:       term: input term.      Returns:       string that is configuration of supplied term.