ipfw.py 7.74 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
"""
    Copyright (c) 2015 Ad Schellevis
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

"""
import os
import tempfile
import subprocess


class IPFW(object):
    def __init__(self):
        pass

36 37
    @staticmethod
    def list_table(table_number):
38 39 40 41
        """ list ipfw table
        :param table_number: ipfw table number
        :return: list
        """
42
        devnull = open(os.devnull, 'w')
43 44
        result = list()
        with tempfile.NamedTemporaryFile() as output_stream:
45
            subprocess.check_call(['/sbin/ipfw', 'table', str(table_number), 'list'],
46
                                  stdout=output_stream,
47
                                  stderr=devnull)
48 49
            output_stream.seek(0)
            for line in output_stream.read().split('\n'):
50
                if line.split(' ')[0].strip() != "":
51 52 53 54 55 56 57 58
                    # process / 32 nets as single addresses to align better with the rule syntax
                    # and local administration.
                    if line.split(' ')[0].split('/')[-1] == '32':
                        # single IPv4 address
                        result.append(line.split(' ')[0].split('/')[0])
                    else:
                        # network
                        result.append(line.split(' ')[0])
59 60
            return result

61
    def ip_or_net_in_table(self, table_number, address):
62 63 64 65 66 67
        """ check if address or net is in this zone's table
        :param table_number: ipfw table number to query
        :param address: ip address or net
        :return: boolean
        """
        ipfw_tbl = self.list_table(table_number)
68
        if address.strip() in ipfw_tbl:
69 70 71 72
            return True

        return False

73 74
    @staticmethod
    def add_to_table(table_number, address):
75 76 77 78 79
        """ add new entry to ipfw table
        :param table_number: ipfw table number
        :param address: ip address or net to add to table
        :return:
        """
80 81
        devnull = open(os.devnull, 'w')
        subprocess.call(['/sbin/ipfw', 'table', table_number, 'add', address], stdout=devnull, stderr=devnull)
82

83 84
    @staticmethod
    def delete_from_table(table_number, address):
85 86 87 88 89
        """ remove entry from ipfw table
        :param table_number: ipfw table number
        :param address: ip address or net to add to table
        :return:
        """
90 91
        devnull = open(os.devnull, 'w')
        subprocess.call(['/sbin/ipfw', 'table', table_number, 'delete', address], stdout=devnull, stderr=devnull)
92

93 94
    @staticmethod
    def list_accounting_info():
95 96 97 98
        """ list accounting info per ip addres, addresses can't overlap in zone's so we just output all we know here
        instead of trying to map addresses back to zones.
        :return: list accounting info per ip address
        """
99
        devnull = open(os.devnull, 'w')
100 101
        result = dict()
        with tempfile.NamedTemporaryFile() as output_stream:
102
            subprocess.check_call(['/sbin/ipfw', '-aT', 'list'],
103
                                  stdout=output_stream,
104
                                  stderr=devnull)
105 106 107 108
            output_stream.seek(0)
            for line in output_stream.read().split('\n'):
                parts = line.split()
                if len(parts) > 5:
109
                    if 30001 <= int(parts[0]) <= 50000 and parts[4] == 'count':
110 111
                        line_pkts = int(parts[1])
                        line_bytes = int(parts[2])
112 113 114 115 116 117 118 119
                        last_accessed = int(parts[3])
                        if parts[7] != 'any':
                            ip_address = parts[7]
                        else:
                            ip_address = parts[9]

                        if ip_address not in result:
                            result[ip_address] = {'rule': int(parts[0]),
120
                                                  'last_accessed': 0,
121 122 123 124
                                                  'in_pkts': 0,
                                                  'in_bytes': 0,
                                                  'out_pkts': 0,
                                                  'out_bytes': 0
125
                                                  }
126 127 128 129 130 131
                        result[ip_address]['last_accessed'] = max(result[ip_address]['last_accessed'],
                                                                  last_accessed)
                        if parts[7] != 'any':
                            # count input
                            result[ip_address]['in_pkts'] = line_pkts
                            result[ip_address]['in_bytes'] = line_bytes
132
                        else:
133 134 135 136
                            # count output
                            result[ip_address]['out_pkts'] = line_pkts
                            result[ip_address]['out_bytes'] = line_bytes

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
            return result

    def add_accounting(self, address):
        """ add ip address for accounting
        :param address: ip address
        :return: None
        """
        # search for unused rule number
        acc_info = self.list_accounting_info()
        if address not in acc_info:
            rule_ids = list()
            for ip_address in acc_info:
                if acc_info[ip_address]['rule'] not in rule_ids:
                    rule_ids.append(acc_info[ip_address]['rule'])

152
            new_rule_id = -1
153 154
            for ruleId in range(30001, 50000):
                if ruleId not in rule_ids:
155
                    new_rule_id = ruleId
156 157 158
                    break

            # add accounting rule
159 160 161 162 163 164
            if new_rule_id != -1:
                devnull = open(os.devnull, 'w')
                subprocess.call(['/sbin/ipfw', 'add', str(new_rule_id), 'count', 'ip', 'from', address, 'to', 'any'],
                                stdout=devnull, stderr=devnull)
                subprocess.call(['/sbin/ipfw', 'add', str(new_rule_id), 'count', 'ip', 'from', 'any', 'to', address],
                                stdout=devnull, stderr=devnull)
165 166 167 168 169 170 171 172

    def del_accounting(self, address):
        """ remove ip address from accounting rules
        :param address: ip address
        :return: None
        """
        acc_info = self.list_accounting_info()
        if address in acc_info:
173
            devnull = open(os.devnull, 'w')
174
            subprocess.call(['/sbin/ipfw', 'delete', str(acc_info[address]['rule'])],
175
                            stdout=devnull, stderr=devnull)
176 177 178 179 180 181 182 183 184

    def delete(self, table_number, address):
        """ remove entry from both ipfw table and accounting rules
        :param table_number: ipfw table number
        :param address: ip address or net to add to table
        :return:
        """
        self.delete_from_table(table_number, address)
        self.del_accounting(address)