├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── pf ├── __init__.py ├── _base.py ├── _struct.py ├── _utils.py ├── constants.py ├── exceptions.py ├── filter.py ├── lib.py ├── queue.py ├── rule.py ├── state.py ├── status.py ├── table.py └── tests │ ├── __init__.py │ ├── cmd.py │ └── test_filter.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[co] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Other 21 | TODO 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Dec 22, 2023 -- version 0.2.3 2 | ----------------------------- 3 | - Updated to OpenBSD 7.4 4 | 5 | 6 | Mar 12, 2022 -- version 0.2.2 7 | ----------------------------- 8 | - Switching to Python 3 on the main branch 9 | - Tested on OpenBSD 7.0 10 | 11 | 12 | May 7, 2019 -- version 0.2.1 13 | ---------------------------- 14 | - Tested on OpenBSD 6.5 15 | 16 | 17 | Nov 23, 2018 -- version 0.2.0 18 | ----------------------------- 19 | - Updated to OpenBSD 6.4 20 | 21 | 22 | Jun 13, 2018 -- version 0.1.9 23 | ----------------------------- 24 | - Updated to OpenBSD 6.3 (many thanks to 25 | [Jasper Lievisse Adriaanse](https://github.com/jasperla) for his contribution) 26 | - Support for syncookies was added to the `PacketFilter` class through 3 new 27 | methods: 28 | - `PacketFilter.get_synflood_watermarks()` 29 | - `PacketFilter.set_synflood_watermarks()` 30 | - `PacketFilter.set_syncookies()` 31 | - Added a new `PFThreshold` class for `max-pkt-rate` thresholds 32 | - Added a new `PFDivert` class to represent divert sockets 33 | 34 | 35 | Jan 15, 2018 -- version 0.1.8 36 | ----------------------------- 37 | - Updated to OpenBSD 6.2 (many thanks to 38 | [Nathan Wheeler](https://github.com/nahun) for his contribution) 39 | - Some little changes were made to the queueing part to integrate flows: 40 | - a new `FlowQueue` object was created 41 | - `PFQueue` objects have a new `flowqueue` attribute 42 | - HFSC_* constants have been replaced with PFQS_* constants 43 | - `PFStatus.since` now contains the number of seconds after machine uptime that 44 | Packet Filter was last started or stopped (not anymore since the epoch). 45 | 46 | 47 | Sep 16, 2016 -- version 0.1.7 48 | ----------------------------- 49 | - Updated to OpenBSD 6.0 50 | 51 | 52 | May 1, 2016 -- version 0.1.6 53 | ----------------------------- 54 | - Updated to OpenBSD 5.9 55 | 56 | 57 | Oct 25, 2015 -- version 0.1.5 58 | ----------------------------- 59 | - Updated to OpenBSD 5.8 60 | 61 | 62 | May 5, 2015 -- version 0.1.4 63 | ---------------------------- 64 | - Updated to OpenBSD 5.7 65 | - Removed `pf._struct.BufferStructure` that was originally meant to overcome 66 | the 1024 bytes limit in `fcntl.ioctl()` and is no longer needed. 67 | 68 | 69 | Nov 6, 2014 -- version 0.1.3 70 | ---------------------------- 71 | - Updated to OpenBSD 5.6 72 | - Removed the last traces of ALTQ 73 | - Fixed a little bug in `PFState._to_string()` 74 | 75 | 76 | May 27, 2014 -- version 0.1.2 77 | ----------------------------- 78 | - Updated to OpenBSD 5.5 79 | - OpenBSD 5.5 has a new queueing system; thus all the queue classes 80 | (`PFAltqCBQ`, `PFAltqHFSC` and `PFAltqPriQ`) and the corresponding stats 81 | classes (`CBQStats`, `HFSCStats` and `PriQStats`) have been replaced by the 82 | `PFQueue` and `PFQueueStats` classes respectively. 83 | - Methods for retrieving and adding queues in the `PacketFilter` class (i.e. 84 | `get_altqs()` and `add_altqs()`) have been replaced (by `get_queues()` and 85 | `load_queues()` respectively). Queues are now cleared along with rules, so 86 | the `clear_altqs()` method has been removed. 87 | 88 | 89 | Nov 10, 2013 -- version 0.1.1 90 | ----------------------------- 91 | - Updated to OpenBSD 5.4 92 | 93 | 94 | May 2, 2013 -- version 0.1.0 95 | ---------------------------- 96 | - Updated to OpenBSD 5.3 97 | 98 | 99 | Oct 21, 2012 -- version 0.0.9 100 | ----------------------------- 101 | - Updated to OpenBSD 5.2 102 | - Printing a `PFIface` object now returns a string similar to the output of the 103 | command `pfctl -sI -vv` 104 | - Fixed a bug in `PFRule` that prevented `rdr-to` rules from being correctly 105 | converted to strings 106 | - Fixed a couple of bugs in the string representation of `PFState` objects 107 | - Added filtering capabilities to `PacketFilter.get_ruleset()`: now it's 108 | possible to retrieve only rules with specific attribute values (e.g. 109 | `filter.get_ruleset(ifname="em0")`) 110 | - Added the `set_optimization()` and `get_optimization()` methods to 111 | `PacketFilter` (thanks Colin!) 112 | - Fixed a bug in `PFAddr._from_str()` which didn't allow interface groups as 113 | addresses (thanks Colin!) 114 | - Added the `pf.lib` module containing some higher-level classes that make 115 | loading PF rules much easier 116 | 117 | 118 | Jun 28, 2012 -- version 0.0.8 119 | ----------------------------- 120 | - Updated to OpenBSD 5.1 121 | - Module renamed to pf for better compliance with PEP8 122 | - Fixed a ZeroDivisionError in PFStatus._to_string() when runtime == 0 123 | - Added support for ALTQ statistics; three new classes have been created 124 | (CBQStats, HFSCStats and PriQStats), corresponding to the schedulers 125 | supported by OpenBSD. 126 | - Fixed a few calls to ctonm() in PFRule.py that didn't pass the af argument 127 | - Added probability and options to the string representation of PFRule 128 | objects 129 | - Fixed a regexp error in PFRule.py that prevented the correct parsing of 130 | some port operands 131 | - Fixed bug in PFUtils.rate2str which prevented correct handling of floating 132 | point numbers 133 | - Test suite completely re-written and run with `python setup.py test` 134 | 135 | 136 | Nov 22, 2012 -- version 0.0.7 137 | ----------------------------- 138 | - Updated to OpenBSD 5.0; the C structures have undergone some minor changes. 139 | - Added support for `divert-*` options in PF rules. 140 | - Added a new `PacketFilter.get_ifaces()` method to retrieve the list of 141 | interfaces and interface drivers known to pf(4). 142 | - Created a new `PFIface` class representing a network interface and returned 143 | by the `PacketFilter.get_ifaces()` method; this class also allows the 144 | retrieval of per-interface statistics. 145 | - Renamed the `PacketFilter.set_ifflag()` method to `PacketFilter.set_ifflags()` 146 | for consistency with `PacketFilter.get_ifflags()`. 147 | 148 | 149 | Jul 9, 2011 -- version 0.0.6 150 | ----------------------------- 151 | - Added support for packet queueing with ALTQ; three new classes have been 152 | created (`PFAltqCBQ`, `PFAltqHFSC` and `PFAltqPriQ`), corresponding to the 153 | schedulers supported by OpenBSD. 154 | 155 | 156 | Jan 19, 2011 -- version 0.0.5 157 | ----------------------------- 158 | - Updated to OpenBSD 4.8; the PF stack has undergone some major changes, such 159 | as removing the different rule types (nat, rdr, binat ... rules do not exist 160 | anymore) and introducing 'match' rules. 161 | This has greatly simplified the `PFRuleset` class and the `PacketFilter` 162 | methods that load/retrieve rules. 163 | - Various bugs have been corrected 164 | - All the code has been reviewed and is now Py3k-ready. 165 | 166 | 167 | Jul 26, 2009 -- version 0.0.4 168 | ----------------------------- 169 | - Updated to OpenBSD-current; modifications include the removal of 'scrub' 170 | rules and making some ioctl() transactional (set loginterface, set hostid, 171 | set reassemble and set debug). 172 | - Added addresses to `PFTable` objects; this should make managing tables much 173 | more user-friendly. 174 | - Added the `PF_RULESET_TABLE` ruleset to `PFRuleset`; this allows loading 175 | tables along with the other rules and doesn't require that the 'persist' flag 176 | be set if the table is not yet referenced by any rule. 177 | - Added the `PacketFilter.set_reassembly()` method. 178 | - Added support for table statistics, by adding the `PFTStats` object and the 179 | `PacketFilter.get_tstats()` and `PacketFilter.clear_tstats()` methods. 180 | 181 | 182 | Mar 22, 2009 -- version 0.0.3 183 | ----------------------------- 184 | - Added the `PFAddr` and `PFPort` classes, representing addresses and ports 185 | respectively. The `PFRuleAddr` class is now a simple container for a 186 | `PFAddr`/`PFPort` pair. 187 | - Added table support trough the `PFTable` and `PFTableAddr` classes; the 188 | apropriate methods for managing tables have been added to the `PacketFilter` 189 | class. 190 | - The `PFPoolAddr` class has been removed: now addresses in `PFPools` are 191 | `PFAddr` instances. 192 | - Re-written the `PFState` class and created the `PFStateKey` class in 193 | accordance with the changes to PF's state handling. 194 | - Added the `PFUid` and `PFGid` classes, representing user and group IDs. 195 | 196 | 197 | Jul 06, 2008 -- version 0.0.2 198 | ----------------------------- 199 | - Added support for loading rulesets, by means of the 200 | `PacketFilter.load_ruleset()` method 201 | - Added the possibility to selectively kill states, based on address family, 202 | transport layer protocol, source and destination addresses and interface 203 | name, thanks to the `PacketFilter.kill_states()` method 204 | - Added the `PacketFilter.set_hostid()` method, which allows you to set the 205 | hostid, a numeric value used by pfsync(4) to identify which host created 206 | state table entries 207 | 208 | 209 | Apr 26, 2008 -- version 0.0.1 210 | ----------------------------- 211 | - Initial release 212 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2022, Daniele Mazzocchio 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the developer nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.md 3 | include CHANGELOG.md 4 | include setup.py 5 | recursive-include pf *.py 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | py-pf 2 | ===== 3 | 4 | `py-pf` is a pure-Python module for managing OpenBSD's Packet Filter. It aims 5 | to add powerful and flexible scripting capabilities to PF, making integration 6 | with third-party software (like IDS, web-based configuration interfaces or 7 | custom management scripts) much easier. 8 | 9 | It runs on Python 3, which is available through OpenBSD's [packages and ports 10 | system](http://www.openbsd.org/faq/faq15.html). 11 | 12 | 13 | Installation 14 | ------------ 15 | Download the source code from [GitHub](https://github.com/dotpy/py-pf) and run 16 | the install script: 17 | 18 | # python setup.py install 19 | 20 | 21 | Tests 22 | ----- 23 | To run the test suite, just run: 24 | 25 | # python setup.py test 26 | 27 | 28 | Documentation 29 | ------------- 30 | A detailed description of the PF module and its classes is available at 31 | http://www.kernel-panic.it/programming/py-pf/. 32 | 33 | A brief list of references, documentation and books about Python, OpenBSD and 34 | Packet Filter can be found at 35 | http://www.kernel-panic.it/software/py-pf/resources.html. 36 | 37 | 38 | Credits 39 | ------- 40 | Copyright (c) 2008-2022 Daniele Mazzocchio (danix@kernel-panic.it). 41 | 42 | Licensed under BSD license (see LICENSE.md file). 43 | -------------------------------------------------------------------------------- /pf/__init__.py: -------------------------------------------------------------------------------- 1 | """A package for managing OpenBSD's Packet Filter.""" 2 | 3 | 4 | __copyright__ = """ 5 | Copyright (c) 2008-2022, Daniele Mazzocchio 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | * Neither the name of the developer nor the names of its contributors may be 17 | used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | """ 31 | 32 | import os 33 | 34 | 35 | __author__ = "Daniele Mazzocchio " 36 | __version__ = "0.2.2" 37 | 38 | __OBSD_VERSION__ = "7.0" 39 | 40 | 41 | from pf.exceptions import PFError 42 | from pf.constants import * 43 | from pf.status import * 44 | from pf.state import * 45 | from pf.table import * 46 | from pf.rule import * 47 | from pf.queue import * 48 | from pf.filter import * 49 | 50 | import pf.lib 51 | 52 | 53 | __all__ = ['PFError', 54 | 'PFStatus', 55 | 'PFIface', 56 | 'PFUid', 57 | 'PFGid', 58 | 'PFAddr', 59 | 'PFPort', 60 | 'PFRuleAddr', 61 | 'PFPool', 62 | 'PFRule', 63 | 'PFRuleset', 64 | 'PFStatePeer', 65 | 'PFStateKey', 66 | 'PFState', 67 | 'PFTableAddr', 68 | 'PFTable', 69 | 'PFTStats', 70 | 'ServiceCurve', 71 | 'PFQueue', 72 | 'PacketFilter'] 73 | 74 | import pf.constants 75 | __all__.extend(os._get_exports_list(pf.constants)) 76 | del pf.constants 77 | -------------------------------------------------------------------------------- /pf/_base.py: -------------------------------------------------------------------------------- 1 | """Base class for all PF-related objects""" 2 | 3 | 4 | __all__ = ['PFObject'] 5 | 6 | 7 | class PFObject(object): 8 | """Base class for wrapper objects around Structures.""" 9 | 10 | _struct_type = None 11 | 12 | def __init__(self, obj=None, **kwargs): 13 | """Check the type of obj and initialize instance attributes.""" 14 | 15 | if self._struct_type is not None and isinstance(obj, self._struct_type): 16 | self._from_struct(obj) 17 | elif isinstance(obj, str): 18 | self._from_string(obj) 19 | 20 | self._from_kwargs(**kwargs) 21 | 22 | def _from_struct(self, struct): 23 | raise NotImplementedError() 24 | 25 | def _from_string(self, line): 26 | raise NotImplementedError() 27 | 28 | def _from_kwargs(self, **kwargs): 29 | for k, v in kwargs.items(): 30 | if hasattr(self, k): 31 | setattr(self, k, v) 32 | else: 33 | raise AttributeError("Unexpected argument: {}".format(k)) 34 | 35 | def _to_struct(self): 36 | raise NotImplementedError() 37 | 38 | def _to_string(self): 39 | raise NotImplementedError() 40 | 41 | def __str__(self): 42 | return self._to_string() 43 | -------------------------------------------------------------------------------- /pf/_struct.py: -------------------------------------------------------------------------------- 1 | """Mapping of C structs, required by ioctl() calls, to ctypes.""" 2 | 3 | from ctypes import * 4 | 5 | from pf.constants import * 6 | 7 | 8 | __all__ = ['timeval', 9 | 'pfioc_limit', 10 | 'pfioc_tm', 11 | 'pf_status', 12 | 'pf_addr_wrap', 13 | 'pf_rule_addr', 14 | 'pfsync_state_peer', 15 | 'pfsync_state_key', 16 | 'pfsync_state', 17 | 'pfioc_states', 18 | 'pfioc_state_kill', 19 | 'pf_pool', 20 | 'pf_rule_uid', 21 | 'pf_rule_gid', 22 | 'pf_threshold', 23 | 'divert', 24 | 'pf_rule', 25 | 'pfioc_rule', 26 | 'pfioc_trans_e', 27 | 'pfioc_trans', 28 | 'pfr_addr', 29 | 'pfr_table', 30 | 'pfioc_table', 31 | 'pfr_tstats', 32 | 'pfi_kif', 33 | 'pfioc_iface', 34 | 'pf_queue_bwspec', 35 | 'pf_queue_scspec', 36 | 'pf_queue_fqspec', 37 | 'pf_queuespec', 38 | 'pfioc_qstats', 39 | 'pfioc_queue', 40 | 'hfsc_sc', 41 | 'hfsc_pktcntr', 42 | 'hfsc_class_stats', 43 | 'queue_stats', 44 | 'ifreq', 45 | 'if_data', 46 | 'pfioc_synflwats'] 47 | 48 | 49 | # Constants 50 | IFNAMSIZ = 16 # From /usr/include/net/if.h 51 | PFRES_MAX = 17 # From /usr/include/net/pfvar.h 52 | LCNT_MAX = 10 # From /usr/include/net/pfvar.h 53 | FCNT_MAX = 3 # From /usr/include/net/pfvar.h 54 | SCNT_MAX = 3 # From /usr/include/net/pfvar.h 55 | PF_MD5_DIGEST_LENGTH = 16 # From /usr/include/net/pfvar.h 56 | PF_TABLE_NAME_SIZE = 32 # From /usr/include/net/pfvar.h 57 | PF_RULE_LABEL_SIZE = 64 # From /usr/include/net/pfvar.h 58 | PF_QNAME_SIZE = 64 # From /usr/include/net/pfvar.h 59 | PF_TAG_NAME_SIZE = 64 # From /usr/include/net/pfvar.h 60 | PF_SKIP_COUNT = 9 # From /usr/include/net/pfvar.h 61 | RTLABEL_LEN = 32 # From /usr/include/net/route.h 62 | PATH_MAX = 1024 # From /usr/include/sys/syslimits.h 63 | 64 | 65 | class pfioc_limit(Structure): # From /usr/include/net/pfvar.h 66 | _fields_ = [("index", c_int), 67 | ("limit", c_uint)] 68 | 69 | 70 | class pfioc_tm(Structure): # From /usr/include/net/pfvar.h 71 | _fields_ = [("timeout", c_int), 72 | ("seconds", c_int)] 73 | 74 | 75 | class pf_status(Structure): # From /usr/include/net/pfvar.h 76 | _fields_ = [("counters", c_uint64 * PFRES_MAX), 77 | ("lcounters", c_uint64 * LCNT_MAX), 78 | ("fcounters", c_uint64 * FCNT_MAX), 79 | ("scounters", c_uint64 * SCNT_MAX), 80 | ("pcounters", c_uint64 * 3 * 2 * 2), 81 | ("bcounters", c_uint64 * 2 * 2), 82 | ("stateid", c_uint64), 83 | ("syncookies_inflight", c_uint64 * 2), 84 | ("since", c_int64), # time_t 85 | ("running", c_uint32), 86 | ("states", c_uint32), 87 | ("states_halfopen", c_uint32), 88 | ("src_nodes", c_uint32), 89 | ("debug", c_uint32), 90 | ("hostid", c_uint32), 91 | ("reass", c_uint32), 92 | ("syncookies_active", c_uint8), 93 | ("syncookies_mode", c_uint8), 94 | ("pad", c_uint8 * 2), 95 | ("ifname", c_char * IFNAMSIZ), 96 | ("pf_chksum", c_uint8 * PF_MD5_DIGEST_LENGTH)] 97 | 98 | 99 | class pf_addr(Structure): # From /usr/include/net/pfvar.h 100 | class _pfa(Union): 101 | _fields_ = [("v4", c_uint32), # struct in_addr 102 | ("v6", c_uint32 * 4), # struct in6_addr 103 | ("addr8", c_uint8 * 16), 104 | ("addr16", c_uint16 * 8), 105 | ("addr32", c_uint32 * 4)] 106 | 107 | _fields_ = [("pfa", _pfa)] 108 | _anonymous_ = ("pfa",) 109 | 110 | 111 | class pf_addr_wrap(Structure): # From /usr/include/net/pfvar.h 112 | class _v(Union): 113 | class _a(Structure): 114 | _fields_ = [("addr", pf_addr), 115 | ("mask", pf_addr)] 116 | 117 | _fields_ = [("a", _a), 118 | ("ifname", c_char * IFNAMSIZ), 119 | ("tblname", c_char * PF_TABLE_NAME_SIZE), 120 | ("rtlabelname", c_char * RTLABEL_LEN), 121 | ("rtlabel", c_uint32)] 122 | 123 | class _p(Union): 124 | _fields_ = [("dyn", c_void_p), # (struct pfi_dynaddr *) 125 | ("tbl", c_void_p), # (struct pfr_ktable *) 126 | ("dyncnt", c_int), 127 | ("tblcnt", c_int)] 128 | 129 | _fields_ = [("v", _v), 130 | ("p", _p), 131 | ("type", c_uint8), 132 | ("iflags", c_uint8)] 133 | 134 | 135 | class pf_rule_addr(Structure): # From /usr/include/net/pfvar.h 136 | _fields_ = [("addr", pf_addr_wrap), 137 | ("port", c_uint16 * 2), 138 | ("neg", c_uint8), 139 | ("port_op", c_uint8), 140 | ("weight", c_uint16)] 141 | 142 | 143 | class pfsync_state_scrub(Structure): # From /usr/include/net/pfvar.h 144 | _fields_ = [("pfss_flags", c_uint16), 145 | ("pfss_ttl", c_uint8), 146 | ("scrub_flag", c_uint8), 147 | ("pfss_ts_mod", c_uint32)] 148 | 149 | 150 | class pfsync_state_peer(Structure): # From /usr/include/net/pfvar.h 151 | _fields_ = [("scrub", pfsync_state_scrub), 152 | ("seqlo", c_uint32), 153 | ("seqhi", c_uint32), 154 | ("seqdiff", c_uint32), 155 | ("max_win", c_uint16), 156 | ("mss", c_uint16), 157 | ("state", c_uint8), 158 | ("wscale", c_uint8), 159 | ("pad", c_uint8 * 6)] 160 | 161 | 162 | class pfsync_state_key(Structure): # From /usr/include/net/pfvar.h 163 | _fields_ = [("addr", pf_addr * 2), 164 | ("port", c_uint16 * 2), 165 | ("rdomain", c_uint16), 166 | ("af", c_uint8), # sa_family_t 167 | ("pad", c_uint8)] 168 | 169 | 170 | class pfsync_state(Structure): # From /usr/include/net/pfvar.h 171 | _fields_ = [("id", c_uint64), 172 | ("ifname", c_char * IFNAMSIZ), 173 | ("key", pfsync_state_key * 2), 174 | ("src", pfsync_state_peer), 175 | ("dst", pfsync_state_peer), 176 | ("rt_addr", pf_addr), 177 | ("rule", c_uint32), 178 | ("anchor", c_uint32), 179 | ("nat_rule", c_uint32), 180 | ("creation", c_uint32), 181 | ("expire", c_uint32), 182 | ("packets", c_uint32 * 2 * 2), 183 | ("bytes", c_uint32 * 2 * 2), 184 | ("creatorid", c_uint32), 185 | ("rtableid", c_int32 * 2), 186 | ("max_mss", c_uint16), 187 | ("af", c_uint8), # sa_family_t 188 | ("proto", c_uint8), 189 | ("direction", c_uint8), 190 | ("log", c_uint8), 191 | ("rt", c_uint8), 192 | ("timeout", c_uint8), 193 | ("sync_flags", c_uint8), 194 | ("updates", c_uint8), 195 | ("min_ttl", c_uint8), 196 | ("set_tos", c_uint8), 197 | ("state_flags", c_uint16), 198 | ("set_prio", c_uint8 * 2)] 199 | 200 | 201 | class pfioc_states(Structure): # From /usr/include/net/pfvar.h 202 | class _ps_u(Union): 203 | _fields_ = [("ps_buf", c_void_p), # caddr_t 204 | ("ps_states", c_void_p)] # struct pfsync_state * 205 | 206 | _fields_ = [("ps_len", c_int), 207 | ("ps_u", _ps_u)] 208 | _anonymous_ = ("ps_u",) 209 | 210 | 211 | class pf_state_cmp(Structure): # From /usr/include/net/pfvar.h 212 | _fields_ = [("id", c_uint64), 213 | ("creatorid", c_uint32), 214 | ("direction", c_uint8), 215 | ("pad", c_uint8 * 3)] 216 | 217 | 218 | class pfioc_state_kill(Structure): # From /usr/include/net/pfvar.h 219 | _fields_ = [("psk_pfcmp", pf_state_cmp), 220 | ("psk_af", c_uint8), # sa_family_t 221 | ("psk_proto", c_int), 222 | ("psk_src", pf_rule_addr), 223 | ("psk_dst", pf_rule_addr), 224 | ("psk_ifname", c_char * IFNAMSIZ), 225 | ("psk_label", c_char * PF_RULE_LABEL_SIZE), 226 | ("psk_killed", c_uint), 227 | ("psk_rdomain", c_uint16)] 228 | 229 | 230 | class pf_poolhashkey(Structure): # From /usr/include/net/pfvar.h 231 | class _pfk(Union): 232 | _fields_ = [("key8", c_uint8 * 16), 233 | ("key16", c_uint16 * 8), 234 | ("key32", c_uint32 * 4)] 235 | 236 | _fields_ = [("pfk", _pfk)] 237 | _anonymous_ = ("pfk",) 238 | 239 | 240 | class pf_pool(Structure): # From /usr/include/net/pfvar.h 241 | _fields_ = [("addr", pf_addr_wrap), 242 | ("key", pf_poolhashkey), 243 | ("counter", pf_addr), 244 | ("ifname", c_char * IFNAMSIZ), 245 | ("kif", c_void_p), # struct pfi_kif * 246 | ("tblidx", c_int), 247 | ("states", c_uint64), 248 | ("curweight", c_int), 249 | ("weight", c_uint16), 250 | ("proxy_port", c_uint16 * 2), 251 | ("port_op", c_uint8), 252 | ("opts", c_uint8)] 253 | 254 | 255 | class pf_rule_ptr(Union): # From /usr/include/net/pfvar.h 256 | _fields_ = [("ptr", c_void_p), # struct pf_rule * 257 | ("nr", c_uint32)] 258 | 259 | 260 | class pf_rule_uid(Structure): # From /usr/include/net/pfvar.h 261 | _fields_ = [("uid", c_uint32 * 2), # uid_t 262 | ("op", c_uint8)] 263 | 264 | 265 | class pf_rule_gid(Structure): # From /usr/include/net/pfvar.h 266 | _fields_ = [("gid", c_uint32 * 2), # uid_t 267 | ("op", c_uint8)] 268 | 269 | 270 | class pf_threshold(Structure): # From /usr/include/net/pfvar.h 271 | _fields_ = [("limit", c_uint32), 272 | ("seconds", c_uint32), 273 | ("count", c_uint32), 274 | ("last", c_uint32)] 275 | 276 | 277 | class divert(Structure): # From /usr/include/net/pfvar.h 278 | _fields_ = [("addr", pf_addr), 279 | ("port", c_uint16), 280 | ("type", c_uint8)] 281 | 282 | 283 | class pf_rule(Structure): # From /usr/include/net/pfvar.h 284 | class _conn_rate(Structure): 285 | _fields_ = [("limit", c_uint32), 286 | ("seconds", c_uint32)] 287 | 288 | _fields_ = [("src", pf_rule_addr), 289 | ("dst", pf_rule_addr), 290 | ("skip", pf_rule_ptr * PF_SKIP_COUNT), 291 | ("label", c_char * PF_RULE_LABEL_SIZE), 292 | ("ifname", c_char * IFNAMSIZ), 293 | ("rcv_ifname", c_char * IFNAMSIZ), 294 | ("qname", c_char * PF_QNAME_SIZE), 295 | ("pqname", c_char * PF_QNAME_SIZE), 296 | ("tagname", c_char * PF_TAG_NAME_SIZE), 297 | ("match_tagname", c_char * PF_TAG_NAME_SIZE), 298 | ("overload_tblname", c_char * PF_TABLE_NAME_SIZE), 299 | ("entries", c_void_p * 2), # TAILQ_ENTRY(pf_rule) 300 | ("nat", pf_pool), 301 | ("rdr", pf_pool), 302 | ("route", pf_pool), 303 | ("pktrate", pf_threshold), 304 | ("evaluations", c_uint64), 305 | ("packets", c_uint64 * 2), 306 | ("bytes", c_uint64 * 2), 307 | ("kif", c_void_p), # (struct pki_kif *) 308 | ("rcv_kif", c_void_p), # (struct pki_kif *) 309 | ("anchor", c_void_p), # (struct pf_anchor *) 310 | ("overload_tbl", c_void_p), # (struct pfr_ktable *) 311 | ("os_fingerprint", c_uint32), # pf_osfp_t 312 | ("rtableid", c_int), 313 | ("onrdomain", c_int), 314 | ("timeout", c_uint32 * PFTM_MAX), 315 | ("states_cur", c_uint32), 316 | ("states_tot", c_uint32), 317 | ("max_states", c_uint32), 318 | ("src_nodes", c_uint32), 319 | ("max_src_nodes", c_uint32), 320 | ("max_src_states", c_uint32), 321 | ("max_src_conn", c_uint32), 322 | ("max_src_conn_rate", _conn_rate), 323 | ("qid", c_uint32), 324 | ("pqid", c_uint32), 325 | ("rt_listid", c_uint32), 326 | ("nr", c_uint32), 327 | ("prob", c_uint32), 328 | ("cuid", c_uint32), # uid_t 329 | ("cpid", c_int32), # pid_t 330 | ("return_icmp", c_uint16), 331 | ("return_icmp6", c_uint16), 332 | ("max_mss", c_uint16), 333 | ("tag", c_uint16), 334 | ("match_tag", c_uint16), 335 | ("scrub_flags", c_uint16), 336 | ("delay", c_uint16), 337 | ("uid", pf_rule_uid), 338 | ("gid", pf_rule_gid), 339 | ("rule_flag", c_uint32), 340 | ("action", c_uint8), 341 | ("direction", c_uint8), 342 | ("log", c_uint8), 343 | ("logif", c_uint8), 344 | ("quick", c_uint8), 345 | ("ifnot", c_uint8), 346 | ("match_tag_not", c_uint8), 347 | ("keep_state", c_uint8), 348 | ("af", c_uint8), # sa_family_t 349 | ("proto", c_uint8), 350 | ("type", c_uint16), 351 | ("code", c_uint16), 352 | ("flags", c_uint8), 353 | ("flagset", c_uint8), 354 | ("min_ttl", c_uint8), 355 | ("allow_opts", c_uint8), 356 | ("rt", c_uint8), 357 | ("return_ttl", c_uint8), 358 | ("tos", c_uint8), 359 | ("set_tos", c_uint8), 360 | ("anchor_relative", c_uint8), 361 | ("anchor_wildcard", c_uint8), 362 | ("flush", c_uint8), 363 | ("prio", c_uint8), 364 | ("set_prio", c_uint8 * 2), 365 | ("naf", c_uint8), # sa_family_t 366 | ("rcvifnot", c_uint8), 367 | ("divert", divert), 368 | ("exptime", c_int64)] # time_t 369 | 370 | 371 | class pfioc_rule(Structure): # From /usr/include/net/pfvar.h 372 | _fields_ = [("action", c_uint32), 373 | ("ticket", c_uint32), 374 | ("nr", c_uint32), 375 | ("anchor", c_char * PATH_MAX), 376 | ("anchor_call", c_char * PATH_MAX), 377 | ("rule", pf_rule)] 378 | 379 | 380 | class pfioc_trans_e(Structure): # From /usr/include/net/pfvar.h 381 | _fields_ = [("type", c_int), 382 | ("anchor", c_char * PATH_MAX), 383 | ("ticket", c_uint32)] 384 | 385 | 386 | class pfioc_trans(Structure): # From /usr/include/net/pfvar.h 387 | _fields_ = [("size", c_int), 388 | ("esize", c_int), 389 | ("array", c_void_p)] # struct pfioc_trans_e * 390 | 391 | 392 | class pfr_addr(Structure): # From /usr/include/net/pfvar.h 393 | class _pfra_u(Union): 394 | _fields_ = [("pfra_ip4addr", c_uint32), # struct in_addr 395 | ("pfra_ip6addr", c_uint32 * 4)] # struct in6_addr 396 | 397 | _fields_ = [("pfra_u", _pfra_u), 398 | ("pfra_ifname", c_char * IFNAMSIZ), 399 | ("pfra_states", c_uint32), 400 | ("pfra_weight", c_uint16), 401 | ("pfra_af", c_uint8), 402 | ("pfra_net", c_uint8), 403 | ("pfra_not", c_uint8), 404 | ("pfra_fback", c_uint8), 405 | ("pfra_type", c_uint8), 406 | ("pad", c_uint8 * 7)] 407 | _anonymous_ = ("pfra_u",) 408 | 409 | 410 | class pfr_table(Structure): # From /usr/include/net/pfvar.h 411 | _fields_ = [("pfrt_anchor", c_char * PATH_MAX), 412 | ("pfrt_name", c_char * PF_TABLE_NAME_SIZE), 413 | ("pfrt_flags", c_uint32), 414 | ("pfrt_fback", c_uint8)] 415 | 416 | 417 | class pfioc_table(Structure): # From /usr/include/net/pfvar.h 418 | _fields_ = [("pfrio_table", pfr_table), 419 | ("pfrio_buffer", c_void_p), 420 | ("pfrio_esize", c_int), 421 | ("pfrio_size", c_int), 422 | ("pfrio_size2", c_int), 423 | ("pfrio_nadd", c_int), 424 | ("pfrio_ndel", c_int), 425 | ("pfrio_nchange", c_int), 426 | ("pfrio_flags", c_int), 427 | ("pfrio_ticket", c_uint32)] 428 | 429 | 430 | class pfr_tstats(Structure): # From /usr/include/net/pfvar.h 431 | _fields_ = [("pfrts_t", pfr_table), 432 | ("pfrts_packets", c_uint64 * PFR_OP_TABLE_MAX * PFR_DIR_MAX), 433 | ("pfrts_bytes", c_uint64 * PFR_OP_TABLE_MAX * PFR_DIR_MAX), 434 | ("pfrts_match", c_uint64), 435 | ("pfrts_nomatch", c_uint64), 436 | ("pfrts_tzero", c_int64), # time_t 437 | ("pfrts_cnt", c_int), 438 | ("pfrts_refcnt", c_int * PFR_REFCNT_MAX)] 439 | 440 | 441 | class pfi_kif(Structure): # From /usr/include/net/pfvar.h 442 | class _RB_ENTRY(Structure): 443 | _fields_ = [("rbe_left", c_void_p), 444 | ("rbe_right", c_void_p), 445 | ("rbe_parent", c_void_p), 446 | ("rbe_color", c_int)] 447 | 448 | _fields_ = [("pfik_name", c_char * IFNAMSIZ), 449 | ("pfik_tree", _RB_ENTRY), 450 | ("pfik_packets", c_uint64 * 2 * 2 * 2), 451 | ("pfik_bytes", c_uint64 * 2 * 2 * 2), 452 | ("pfik_tzero", c_int64), # time_t 453 | ("pfik_flags", c_int), 454 | ("pfik_flags_new", c_int), 455 | ("pfik_ah_cookie", c_void_p), 456 | ("pfik_ifp", c_void_p), # (struct ifnet *) 457 | ("pfik_group", c_void_p), # (struct ifg_group *) 458 | ("pfik_states", c_int), 459 | ("pfik_rules", c_int), 460 | ("pfik_routes", c_int), 461 | ("pfik_srcnodes", c_int), 462 | ("pfik_flagrefs", c_int), 463 | ("pfik_dynaddrs", c_void_p * 2)] # TAILQ_HEAD(,pfi_dynaddr) 464 | 465 | 466 | class pfioc_iface(Structure): # From /usr/include/net/pfvar.h 467 | _fields_ = [("pfiio_name", c_char * IFNAMSIZ), 468 | ("pfiio_buffer", c_void_p), 469 | ("pfiio_esize", c_int), 470 | ("pfiio_size", c_int), 471 | ("pfiio_nzero", c_int), 472 | ("pfiio_flags", c_int)] 473 | 474 | 475 | class timeval(Structure): # From /usr/include/sys/time.h 476 | _fields_ = [("tv_sec", c_int64), # time_t 477 | ("tv_usec", c_long)] # suseconds_t 478 | 479 | 480 | class pf_queue_bwspec(Structure): # From /usr/include/net/pfvar.h 481 | _fields_ = [("absolute", c_uint), 482 | ("percent", c_uint)] 483 | 484 | 485 | class pf_queue_scspec(Structure): # From /usr/include/net/pfvar.h 486 | _fields_ = [("m1", pf_queue_bwspec), 487 | ("m2", pf_queue_bwspec), 488 | ("d", c_uint)] 489 | 490 | 491 | class pf_queue_fqspec(Structure): # From /usr/include/net/pfvar.h 492 | _fields_ = [("flows", c_uint), 493 | ("quantum", c_uint), 494 | ("target", c_uint), 495 | ("interval", c_uint)] 496 | 497 | 498 | class pf_queuespec(Structure): # From /usr/include/net/pfvar.h 499 | _fields_ = [("entries", c_void_p * 2), # TAILQ_ENTRY(pf_queuespec) 500 | ("qname", c_char * PF_QNAME_SIZE), 501 | ("parent", c_char * PF_QNAME_SIZE), 502 | ("ifname", c_char * IFNAMSIZ), 503 | ("realtime", pf_queue_scspec), 504 | ("linkshare", pf_queue_scspec), 505 | ("upperlimit", pf_queue_scspec), 506 | ("flowqueue", pf_queue_fqspec), 507 | ("kif", c_void_p), # struct pfi_kif * 508 | ("flags", c_uint), 509 | ("qlimit", c_uint), 510 | ("qid", c_uint32), 511 | ("parent_qid", c_uint32)] 512 | 513 | 514 | class pfioc_qstats(Structure): # From /usr/include/net/pfvar.h 515 | _fields_ = [("ticket", c_uint32), 516 | ("nr", c_uint32), 517 | ("queue", pf_queuespec), 518 | ("buf", c_void_p), 519 | ("nbytes", c_int)] 520 | 521 | 522 | class pfioc_queue(Structure): # From /usr/include/net/pfvar.h 523 | _fields_ = [("ticket", c_uint32), 524 | ("nr", c_uint), 525 | ("queue", pf_queuespec)] 526 | 527 | 528 | class hfsc_sc(Structure): # From /usr/include/net/hfsc.h 529 | _fields_ = [("m1", c_uint), 530 | ("d", c_uint), 531 | ("m2", c_uint)] 532 | 533 | 534 | class hfsc_pktcntr(Structure): # From /usr/include/net/hfsc.h 535 | _fields_ = [("packets", c_uint64), 536 | ("bytes", c_uint64)] 537 | 538 | 539 | class hfsc_class_stats(Structure): # From /usr/include/net/hfsc.h 540 | _fields_ = [("xmit_cnt", hfsc_pktcntr), 541 | ("drop_cnt", hfsc_pktcntr), 542 | ("qlength", c_uint), 543 | ("qlimit", c_uint), 544 | ("period", c_uint), 545 | ("class_id", c_uint), 546 | ("class_handle", c_uint32), 547 | ("rsc", hfsc_sc), 548 | ("fsc", hfsc_sc), 549 | ("usc", hfsc_sc), 550 | ("total", c_uint64), 551 | ("cumul", c_uint64), 552 | ("d", c_uint64), 553 | ("e", c_uint64), 554 | ("vt", c_uint64), 555 | ("f", c_uint64), 556 | ("initvt", c_uint64), 557 | ("vtoff", c_uint64), 558 | ("cvtmax", c_uint64), 559 | ("myf", c_uint64), 560 | ("cfmin", c_uint64), 561 | ("cvtmin", c_uint64), 562 | ("myfadj", c_uint64), 563 | ("vtadj", c_uint64), 564 | ("cur_time", c_uint64), 565 | ("machclk_freq", c_uint32), 566 | ("vtperiod", c_uint), 567 | ("parentperiod", c_uint), 568 | ("nactive", c_int), 569 | ("qtype", c_int)] 570 | 571 | 572 | class queue_stats(Structure): # From /usr/src/sbin/pfctl/pfctl_queue.c 573 | _fields_ = [("data", hfsc_class_stats), 574 | ("avgn", c_int), 575 | ("avg_bytes", c_double), 576 | ("avg_packets", c_double), 577 | ("prev_bytes", c_uint64), 578 | ("prev_packets", c_uint64)] 579 | 580 | 581 | class ifreq(Structure): # From /usr/include/net/if.h 582 | class _ifr_ifru(Union): 583 | class _sockaddr(Structure): # From /usr/include/sys/socket.h 584 | _fields_ = [("sa_len", c_uint8), 585 | ("sa_family", c_uint8), # sa_family_t 586 | ("sa_data", c_char * 14)] 587 | 588 | _fields_ = [("ifru_addr", _sockaddr), 589 | ("ifru_dstaddr", _sockaddr), 590 | ("ifru_broadaddr", _sockaddr), 591 | ("ifru_flags", c_short), 592 | ("ifru_metric", c_int), 593 | ("ifru_vnetid", c_int64), 594 | ("ifru_media", c_uint64), 595 | ("ifru_data", c_char_p), # caddr_t 596 | ("ifru_index", c_uint)] 597 | 598 | _fields_ = [("ifr_name", c_char * IFNAMSIZ), 599 | ("ifr_ifru", _ifr_ifru)] 600 | _anonymous_ = ("ifr_ifru",) 601 | 602 | 603 | class if_data(Structure): # From /usr/include/net/if.h 604 | _fields_ = [("ifi_type", c_ubyte), 605 | ("ifi_addrlen", c_ubyte), 606 | ("ifi_hdrlen", c_ubyte), 607 | ("ifi_link_state", c_ubyte), 608 | ("ifi_mtu", c_uint32), 609 | ("ifi_metric", c_uint32), 610 | ("ifi_rdomain", c_uint32), 611 | ("ifi_baudrate", c_uint64), 612 | ("ifi_ipackets", c_uint64), 613 | ("ifi_ierrors", c_uint64), 614 | ("ifi_opackets", c_uint64), 615 | ("ifi_oerrors", c_uint64), 616 | ("ifi_collisions", c_uint64), 617 | ("ifi_ibytes", c_uint64), 618 | ("ifi_obytes", c_uint64), 619 | ("ifi_imcasts", c_uint64), 620 | ("ifi_omcasts", c_uint64), 621 | ("ifi_iqdrops", c_uint64), 622 | ("ifi_oqdrops", c_uint64), 623 | ("ifi_noproto", c_uint64), 624 | ("ifi_capabilities", c_uint32), 625 | ("ifi_lastchange", timeval)] 626 | 627 | 628 | class pfioc_synflwats(Structure): # From /usr/include/net/pfvar.h 629 | _fields_ = [("hiwat", c_uint32), 630 | ("lowat", c_uint32)] 631 | -------------------------------------------------------------------------------- /pf/_utils.py: -------------------------------------------------------------------------------- 1 | """Miscellaneous network and PF-related utilities""" 2 | 3 | 4 | import re 5 | import glob 6 | import time 7 | import ctypes 8 | from socket import * 9 | from fcntl import ioctl 10 | 11 | from pf.constants import * 12 | from pf.exceptions import PFError 13 | from pf._struct import ifreq, if_data, timeval 14 | 15 | 16 | # Dictionaries for mapping strings to constants 17 | # Debug levels 18 | dbg_levels = { 19 | "emerg": LOG_EMERG, 20 | "alert": LOG_ALERT, 21 | "crit": LOG_CRIT, 22 | "err": LOG_ERR, 23 | "warn": LOG_WARNING, 24 | "notice": LOG_NOTICE, 25 | "info": LOG_INFO, 26 | "debug": LOG_DEBUG 27 | } 28 | 29 | # Memory limits 30 | pf_limits = { 31 | "states": PF_LIMIT_STATES, 32 | "src-nodes": PF_LIMIT_SRC_NODES, 33 | "frags": PF_LIMIT_FRAGS, 34 | "tables": PF_LIMIT_TABLES, 35 | "table-entries": PF_LIMIT_TABLE_ENTRIES 36 | } 37 | 38 | # Ports, UIDs and GIDs operators 39 | pf_ops = { 40 | "": PF_OP_NONE, 41 | "><": PF_OP_IRG, 42 | "<>": PF_OP_XRG, 43 | "=": PF_OP_EQ, 44 | "!=": PF_OP_NE, 45 | "<": PF_OP_LT, 46 | "<=": PF_OP_LE, 47 | ">": PF_OP_GT, 48 | ">=": PF_OP_GE, 49 | ":": PF_OP_RRG 50 | } 51 | 52 | # Interface modifiers 53 | pf_if_mods = { 54 | "network": PFI_AFLAG_NETWORK, 55 | "broadcast": PFI_AFLAG_BROADCAST, 56 | "peer": PFI_AFLAG_PEER, 57 | "0": PFI_AFLAG_NOALIAS 58 | } 59 | 60 | # Global timeouts 61 | pf_timeouts = { 62 | "tcp.first": PFTM_TCP_FIRST_PACKET, 63 | "tcp.opening": PFTM_TCP_OPENING, 64 | "tcp.established": PFTM_TCP_ESTABLISHED, 65 | "tcp.closing": PFTM_TCP_CLOSING, 66 | "tcp.finwait": PFTM_TCP_FIN_WAIT, 67 | "tcp.closed": PFTM_TCP_CLOSED, 68 | "tcp.tsdiff": PFTM_TS_DIFF, 69 | "udp.first": PFTM_UDP_FIRST_PACKET, 70 | "udp.single": PFTM_UDP_SINGLE, 71 | "udp.multiple": PFTM_UDP_MULTIPLE, 72 | "icmp.first": PFTM_ICMP_FIRST_PACKET, 73 | "icmp.error": PFTM_ICMP_ERROR_REPLY, 74 | "other.first": PFTM_OTHER_FIRST_PACKET, 75 | "other.single": PFTM_OTHER_SINGLE, 76 | "other.multiple": PFTM_OTHER_MULTIPLE, 77 | "frag": PFTM_FRAG, 78 | "interval": PFTM_INTERVAL, 79 | "adaptive.start": PFTM_ADAPTIVE_START, 80 | "adaptive.end": PFTM_ADAPTIVE_END, 81 | "src.track": PFTM_SRC_NODE 82 | } 83 | 84 | # Syncookies modes 85 | pf_syncookies_modes = { 86 | "never": PF_SYNCOOKIES_NEVER, 87 | "always": PF_SYNCOOKIES_ALWAYS, 88 | "adaptive": PF_SYNCOOKIES_ADAPTIVE 89 | } 90 | 91 | # PF Optimization Hints 92 | pf_hint_normal = { 93 | "tcp.first": 2 * 60, 94 | "tcp.opening": 30, 95 | "tcp.established": 24 * 60 * 60, 96 | "tcp.closing": 15 * 60, 97 | "tcp.finwait": 45, 98 | "tcp.closed": 90, 99 | "tcp.tsdiff": 30 100 | } 101 | 102 | pf_hint_sattelite = { 103 | "tcp.first": 3 * 60, 104 | "tcp.opening": 30 + 5, 105 | "tcp.established": 24 * 60 * 60, 106 | "tcp.closing": 15 * 60 + 5, 107 | "tcp.finwait": 45 + 5, 108 | "tcp.closed": 90 + 5, 109 | "tcp.tsdiff": 60 110 | } 111 | 112 | pf_hint_conservative = { 113 | "tcp.first": 60 * 60, 114 | "tcp.opening": 15 * 60, 115 | "tcp.established": 5 * 24 * 60 * 60, 116 | "tcp.closing": 60 * 60, 117 | "tcp.finwait": 10 * 60, 118 | "tcp.closed": 3 * 90, 119 | "tcp.tsdiff": 60 120 | } 121 | 122 | pf_hint_aggressive = { 123 | "tcp.first": 30, 124 | "tcp.opening": 5, 125 | "tcp.established": 5 * 60 * 60, 126 | "tcp.closing": 60, 127 | "tcp.finwait": 30, 128 | "tcp.closed": 30, 129 | "tcp.tsdiff": 10 130 | } 131 | 132 | pf_hints = { 133 | "normal": pf_hint_normal, 134 | "sattelite": pf_hint_sattelite, 135 | "high-latency": pf_hint_sattelite, 136 | "conservative": pf_hint_conservative, 137 | "aggressive": pf_hint_aggressive 138 | } 139 | 140 | 141 | # Dictionaries for mapping constants to strings 142 | # TCP states 143 | tcpstates = {TCPS_CLOSED: "CLOSED", 144 | TCPS_LISTEN: "LISTEN", 145 | TCPS_SYN_SENT: "SYN_SENT", 146 | TCPS_SYN_RECEIVED: "SYN_RCVD", 147 | TCPS_ESTABLISHED: "ESTABLISHED", 148 | TCPS_CLOSE_WAIT: "CLOSE_WAIT", 149 | TCPS_FIN_WAIT_1: "FIN_WAIT_1", 150 | TCPS_CLOSING: "CLOSING", 151 | TCPS_LAST_ACK: "LAST_ACK", 152 | TCPS_FIN_WAIT_2: "FIN_WAIT_2", 153 | TCPS_TIME_WAIT: "TIME_WAIT"} 154 | 155 | # UDP states 156 | udpstates = {PFUDPS_NO_TRAFFIC: "NO_TRAFFIC", 157 | PFUDPS_SINGLE: "SINGLE", 158 | PFUDPS_MULTIPLE: "MULTIPLE"} 159 | 160 | # ICMP and ICMPv6 codes and types 161 | icmp_codes = { 162 | (ICMP_UNREACH, ICMP_UNREACH_NET): "net-unr", 163 | (ICMP_UNREACH, ICMP_UNREACH_HOST): "host-unr", 164 | (ICMP_UNREACH, ICMP_UNREACH_PROTOCOL): "proto-unr", 165 | (ICMP_UNREACH, ICMP_UNREACH_PORT): "port-unr", 166 | (ICMP_UNREACH, ICMP_UNREACH_NEEDFRAG): "needfrag", 167 | (ICMP_UNREACH, ICMP_UNREACH_SRCFAIL): "srcfail", 168 | (ICMP_UNREACH, ICMP_UNREACH_NET_UNKNOWN): "net-unk", 169 | (ICMP_UNREACH, ICMP_UNREACH_HOST_UNKNOWN): "host-unk", 170 | (ICMP_UNREACH, ICMP_UNREACH_ISOLATED): "isolate", 171 | (ICMP_UNREACH, ICMP_UNREACH_NET_PROHIB): "net-prohib", 172 | (ICMP_UNREACH, ICMP_UNREACH_HOST_PROHIB): "host-prohib", 173 | (ICMP_UNREACH, ICMP_UNREACH_TOSNET): "net-tos", 174 | (ICMP_UNREACH, ICMP_UNREACH_TOSHOST): "host-tos", 175 | (ICMP_UNREACH, ICMP_UNREACH_FILTER_PROHIB): "filter-prohib", 176 | (ICMP_UNREACH, ICMP_UNREACH_HOST_PRECEDENCE): "host-preced", 177 | (ICMP_UNREACH, ICMP_UNREACH_PRECEDENCE_CUTOFF): "cutoff-preced", 178 | (ICMP_REDIRECT, ICMP_REDIRECT_NET): "redir-net", 179 | (ICMP_REDIRECT, ICMP_REDIRECT_HOST): "redir-host", 180 | (ICMP_REDIRECT, ICMP_REDIRECT_TOSNET): "redir-tos-net", 181 | (ICMP_REDIRECT, ICMP_REDIRECT_TOSHOST): "redir-tos-host", 182 | (ICMP_ROUTERADVERT, ICMP_ROUTERADVERT_NORMAL): "normal-adv", 183 | (ICMP_ROUTERADVERT, ICMP_ROUTERADVERT_NOROUTE_COMMON): "common-adv", 184 | (ICMP_TIMXCEED, ICMP_TIMXCEED_INTRANS): "transit", 185 | (ICMP_TIMXCEED, ICMP_TIMXCEED_REASS): "reassemb", 186 | (ICMP_PARAMPROB, ICMP_PARAMPROB_ERRATPTR): "badhead", 187 | (ICMP_PARAMPROB, ICMP_PARAMPROB_OPTABSENT): "optmiss", 188 | (ICMP_PARAMPROB, ICMP_PARAMPROB_LENGTH): "badlen", 189 | (ICMP_PHOTURIS, ICMP_PHOTURIS_UNKNOWN_INDEX): "unknown-ind", 190 | (ICMP_PHOTURIS, ICMP_PHOTURIS_AUTH_FAILED): "auth-fail", 191 | (ICMP_PHOTURIS, ICMP_PHOTURIS_DECRYPT_FAILED): "decrypt-fail"} 192 | 193 | icmp6_codes = { 194 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_ADMIN): "admin-unr", 195 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_NOROUTE): "noroute-unr", 196 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_NOTNEIGHBOR): "notnbr-unr", 197 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_BEYONDSCOPE): "beyond-unr", 198 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_ADDR): "addr-unr", 199 | (ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_NOPORT): "port-unr", 200 | (ICMP6_TIME_EXCEEDED, ICMP6_TIME_EXCEED_TRANSIT): "transit", 201 | (ICMP6_TIME_EXCEEDED, ICMP6_TIME_EXCEED_REASSEMBLY): "reassemb", 202 | (ICMP6_PARAM_PROB, ICMP6_PARAMPROB_HEADER): "badhead", 203 | (ICMP6_PARAM_PROB, ICMP6_PARAMPROB_NEXTHEADER): "nxthdr", 204 | (ND_REDIRECT, ND_REDIRECT_ONLINK): "redironlink", 205 | (ND_REDIRECT, ND_REDIRECT_ROUTER): "redirrouter"} 206 | 207 | icmp_types = { 208 | ICMP_ECHO: "echoreq", 209 | ICMP_ECHOREPLY: "echorep", 210 | ICMP_UNREACH: "unreach", 211 | ICMP_SOURCEQUENCH: "squench", 212 | ICMP_REDIRECT: "redir", 213 | ICMP_ALTHOSTADDR: "althost", 214 | ICMP_ROUTERADVERT: "routeradv", 215 | ICMP_ROUTERSOLICIT: "routersol", 216 | ICMP_TIMXCEED: "timex", 217 | ICMP_PARAMPROB: "paramprob", 218 | ICMP_TSTAMP: "timereq", 219 | ICMP_TSTAMPREPLY: "timerep", 220 | ICMP_IREQ: "inforeq", 221 | ICMP_IREQREPLY: "inforep", 222 | ICMP_MASKREQ: "maskreq", 223 | ICMP_MASKREPLY: "maskrep", 224 | ICMP_TRACEROUTE: "trace", 225 | ICMP_DATACONVERR: "dataconv", 226 | ICMP_MOBILE_REDIRECT: "mobredir", 227 | ICMP_IPV6_WHEREAREYOU: "ipv6-where", 228 | ICMP_IPV6_IAMHERE: "ipv6-here", 229 | ICMP_MOBILE_REGREQUEST: "mobregreq", 230 | ICMP_MOBILE_REGREPLY: "mobregrep", 231 | ICMP_SKIP: "skip", 232 | ICMP_PHOTURIS: "photuris"} 233 | 234 | icmp6_types = { 235 | ICMP6_DST_UNREACH: "unreach", 236 | ICMP6_PACKET_TOO_BIG: "toobig", 237 | ICMP6_TIME_EXCEEDED: "timex", 238 | ICMP6_PARAM_PROB: "paramprob", 239 | ICMP6_ECHO_REQUEST: "echoreq", 240 | ICMP6_ECHO_REPLY: "echorep", 241 | ICMP6_MEMBERSHIP_QUERY: "groupqry", 242 | MLD_LISTENER_QUERY: "listqry", 243 | ICMP6_MEMBERSHIP_REPORT: "grouprep", 244 | MLD_LISTENER_REPORT: "listenrep", 245 | ICMP6_MEMBERSHIP_REDUCTION: "groupterm", 246 | MLD_LISTENER_DONE: "listendone", 247 | ND_ROUTER_SOLICIT: "routersol", 248 | ND_ROUTER_ADVERT: "routeradv", 249 | ND_NEIGHBOR_SOLICIT: "neighbrsol", 250 | ND_NEIGHBOR_ADVERT: "neighbradv", 251 | ND_REDIRECT: "redir", 252 | ICMP6_ROUTER_RENUMBERING: "routrrenum", 253 | ICMP6_WRUREQUEST: "wrureq", 254 | ICMP6_WRUREPLY: "wrurep", 255 | ICMP6_FQDN_QUERY: "fqdnreq", 256 | ICMP6_FQDN_REPLY: "fqdnrep", 257 | ICMP6_NI_QUERY: "niqry", 258 | ICMP6_NI_REPLY: "nirep", 259 | MLD_MTRACE_RESP: "mtraceresp", 260 | MLD_MTRACE: "mtrace"} 261 | 262 | 263 | # Helper functions 264 | def getprotobynumber(number, file="/etc/protocols"): 265 | """Map a protocol number to a name. 266 | 267 | Return the protocol name or None if no match is found. 268 | """ 269 | r = re.compile("(?P\S+)\s+(?P\d+)") 270 | with open(file, 'r') as f: 271 | for line in f: 272 | m = r.match(line) 273 | if m and int(m.group("num")) == number: 274 | return m.group("proto") 275 | 276 | 277 | def geticmpcodebynumber(type, code, af): 278 | """Return the ICMP code as a string.""" 279 | ic = icmp_codes if (af != AF_INET6) else icmp6_codes 280 | try: 281 | return ic[(type, code)] 282 | except KeyError: 283 | return None 284 | 285 | 286 | def geticmptypebynumber(type, af): 287 | """Return the ICMP type as a string.""" 288 | it = icmp_types if (af != AF_INET6) else icmp6_types 289 | try: 290 | return it[type] 291 | except KeyError: 292 | return None 293 | 294 | 295 | def ctonm(cidr, af): 296 | """Convert netmask from CIDR to dotted decimal notation.""" 297 | try: 298 | l = {AF_INET: 32, AF_INET6: 128}[af] 299 | except KeyError: 300 | raise ValueError("Invalid address family") 301 | 302 | b = "1" * cidr + "0" * (l - cidr) 303 | mask = "".join([chr(int(b[i:i+8], 2)) for i in range(0, l, 8)]) 304 | mask = bytearray([int(b[i:i+8], 2) for i in range(0, l, 8)]) 305 | 306 | return inet_ntop(af, mask) 307 | 308 | 309 | def nmtoc(netmask, af): 310 | """Convert netmask from dotted decimal to CIDR notation.""" 311 | cidr = 0 312 | for b in inet_pton(af, netmask): 313 | cidr += bin(b).count("1") 314 | # for b in map(ord, inet_pton(af, netmask)): 315 | # while b: 316 | # cidr += b & 1 317 | # b >>= 1 318 | # 319 | return cidr 320 | 321 | def is_IPaddr(addr): 322 | """Return True if addr is a valid IPv4 address""" 323 | return _is_valid_addr(AF_INET, addr) 324 | 325 | def is_IP6addr(addr): 326 | """Return True if addr is a valid IPv6 address""" 327 | return _is_valid_addr(AF_INET6, addr) 328 | 329 | def _is_valid_addr(af, addr): 330 | """Return True is addr is a valid address in the address family specified""" 331 | try: 332 | inet_pton(af, addr) 333 | except error: # socket.error 334 | return False 335 | return True 336 | 337 | def rate2str(bw): 338 | """Return the string representation of the network speed rate.""" 339 | units = [" ", "K", "M", "G"] 340 | for unit in units: 341 | if bw >= 1000: 342 | bw /= 1000.0 343 | else: 344 | break 345 | 346 | if int(bw * 100 % 100): 347 | return "{:.2f}{}".format(bw, unit) 348 | else: 349 | return "{}{}".format(int(bw), unit) 350 | 351 | def getifmtu(ifname): 352 | """Quick hack to get MTU and speed for a specified interface.""" 353 | from pf.filter import _IOWR 354 | SIOCGIFMTU = _IOWR('i', 126, ifreq) 355 | s = socket(AF_INET, SOCK_DGRAM) 356 | ifrdat = if_data() 357 | ifr = ifreq(ifr_name=ifname, ifru_data=ctypes.addressof(ifrdat)) 358 | 359 | try: 360 | ioctl(s, SIOCGIFMTU, ifr.asBuffer()) 361 | except IOError: 362 | pass 363 | 364 | s.close() 365 | mtu = (ifr.ifru_metric if (ifr.ifru_metric > 0) else 1500) 366 | speed = ifrdat.ifi_baudrate 367 | 368 | return (mtu, speed) 369 | 370 | def uptime(): 371 | """Return system uptime in seconds""" 372 | CTL_KERN = 1 # From /usr/include/sys/sysctl.h 373 | KERN_BOOTTIME = 21 # From /usr/include/sys/sysctl.h 374 | 375 | mib = (ctypes.c_int * 2)(CTL_KERN, KERN_BOOTTIME) 376 | tv = timeval() 377 | size = ctypes.c_size_t(ctypes.sizeof(timeval)) 378 | 379 | libc = ctypes.CDLL(glob.glob("/usr/lib/libc.so*")[0], use_errno=True) 380 | libc.sysctl.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, 381 | ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] 382 | if libc.sysctl(mib, 2, ctypes.addressof(tv), 383 | ctypes.addressof(size), 0, 0) == -1: 384 | raise PFError("Call to sysctl() failed") 385 | 386 | return int(time.time()) - tv.tv_sec 387 | -------------------------------------------------------------------------------- /pf/constants.py: -------------------------------------------------------------------------------- 1 | """Export constants shared by all classes of the module.""" 2 | 3 | 4 | # Actions (from /usr/include/net/pfvar.h) 5 | PF_PASS = 0 6 | PF_DROP = 1 7 | PF_SCRUB = 2 8 | PF_NOSCRUB = 3 9 | PF_NAT = 4 10 | PF_NONAT = 5 11 | PF_BINAT = 6 12 | PF_NOBINAT = 7 13 | PF_RDR = 8 14 | PF_NORDR = 9 15 | PF_SYNPROXY_DROP = 10 16 | PF_DEFER = 11 17 | PF_MATCH = 12 18 | PF_DIVERT = 13 19 | PF_RT = 14 20 | PF_AFRT = 15 21 | 22 | # PF transaction types (from /usr/include/net/pfvar.h) 23 | PF_TRANS_RULESET = 0 24 | PF_TRANS_TABLE = 1 25 | 26 | # PF rule flags (from /usr/include/net/pfvar.h) 27 | PFRULE_DROP = 0x000000 28 | PFRULE_RETURNRST = 0x000001 29 | PFRULE_FRAGMENT = 0x000002 30 | PFRULE_RETURNICMP = 0x000004 31 | PFRULE_RETURN = 0x000008 32 | PFRULE_NOSYNC = 0x000010 33 | PFRULE_SRCTRACK = 0x000020 34 | PFRULE_RULESRCTRACK = 0x000040 35 | PFRULE_SETDELAY = 0x000080 36 | PFRULE_IFBOUND = 0x010000 37 | PFRULE_STATESLOPPY = 0x020000 38 | PFRULE_PFLOW = 0x040000 39 | PFRULE_ONCE = 0x100000 40 | PFRULE_AFTO = 0x200000 41 | PFRULE_EXPIRED = 0x400000 42 | 43 | # PF rule flags (from /usr/include/net/pfvar.h) 44 | PFRULE_IFBOUND = 0x00010000 45 | PFRULE_STATESLOPPY = 0x00020000 46 | PFRULE_PFLOW = 0x00040000 47 | PFRULE_ONCE = 0x00100000 48 | PFRULE_AFTO = 0x00200000 49 | 50 | # Port comparison operators (from /usr/include/net/pfvar.h) 51 | PF_OP_NONE = 0 52 | PF_OP_IRG = 1 53 | PF_OP_EQ = 2 54 | PF_OP_NE = 3 55 | PF_OP_LT = 4 56 | PF_OP_LE = 5 57 | PF_OP_GT = 6 58 | PF_OP_GE = 7 59 | PF_OP_XRG = 8 60 | PF_OP_RRG = 9 61 | 62 | # Rules retrieval options (from /usr/include/net/pfvar.h) 63 | PF_GET_NONE = 0 64 | PF_GET_CLR_CNTR = 1 65 | 66 | # PF keep states (from /usr/include/net/pfvar.h) 67 | PF_STATE_NORMAL = 0x1 68 | PF_STATE_MODULATE = 0x2 69 | PF_STATE_SYNPROXY = 0x3 70 | 71 | # Routing options (from /usr/include/net/pfvar.h) 72 | PF_NOPFROUTE = 0 73 | PF_ROUTETO = 1 74 | PF_DUPTO = 2 75 | PF_REPLYTO = 3 76 | 77 | # State keys (from /usr/include/net/pfvar.h) 78 | PF_SK_WIRE = 0 79 | PF_SK_STACK = 1 80 | PF_SK_BOTH = 2 81 | 82 | # Log options (from /usr/include/net/pfvar.h) 83 | PF_LOG = 0x01 84 | PF_LOG_ALL = 0x02 85 | PF_LOG_SOCKET_LOOKUP = 0x04 86 | PF_LOG_FORCE = 0x08 87 | PF_LOG_MATCHES = 0x10 88 | 89 | # Address types (from /usr/include/net/pfvar.h) 90 | PF_ADDR_ADDRMASK = 0 91 | PF_ADDR_NOROUTE = 1 92 | PF_ADDR_DYNIFTL = 2 93 | PF_ADDR_TABLE = 3 94 | PF_ADDR_RTLABEL = 4 95 | PF_ADDR_URPFFAILED = 5 96 | PF_ADDR_RANGE = 6 97 | PF_ADDR_NONE = 7 98 | 99 | # OS fingerprints matches (from /usr/include/net/pfvar.h) 100 | PF_OSFP_ANY = 0 101 | PF_OSFP_UNKNOWN = -1 102 | PF_OSFP_NOMATCH = -2 103 | 104 | # Interface flags (from /usr/include/net/pfvar.h) 105 | PFI_AFLAG_NETWORK = 0x01 106 | PFI_AFLAG_BROADCAST = 0x02 107 | PFI_AFLAG_PEER = 0x04 108 | PFI_AFLAG_MODEMASK = 0x07 109 | PFI_AFLAG_NOALIAS = 0x08 110 | 111 | # Traffic directions (from /usr/include/net/pfvar.h) 112 | PF_INOUT = 0 113 | PF_IN = 1 114 | PF_OUT = 2 115 | PF_FWD = 3 116 | 117 | # Flush options (from /usr/include/net/pfvar.h) 118 | PF_FLUSH = 0x01 119 | PF_FLUSH_GLOBAL = 0x02 120 | 121 | # IP type of service (from /usr/include/netinet/ip.h) 122 | IPTOS_LOWDELAY = 0x10 123 | IPTOS_THROUGHPUT = 0x08 124 | IPTOS_RELIABILITY = 0x04 125 | 126 | # NAT ports range (from /usr/src/sbin/pfctl/pfctl_parser.h) 127 | PF_NAT_PROXY_PORT_LOW = 50001 128 | PF_NAT_PROXY_PORT_HIGH = 65535 129 | 130 | # Thresholds for syncookies (from /usr/include/net/pfvar.h) 131 | PF_THRESHOLD_MULT = 1000 132 | PF_THRESHOLD_MAX = 0xffffffff / PF_THRESHOLD_MULT 133 | 134 | # Divert types (from /usr/include/net/pfvar.h) 135 | PF_DIVERT_NONE = 0 136 | PF_DIVERT_TO = 1 137 | PF_DIVERT_REPLY = 2 138 | PF_DIVERT_PACKET = 3 139 | 140 | # Syncookies (from /usr/include/net/pfvar.h) 141 | PF_SYNCOOKIES_NEVER = 0 142 | PF_SYNCOOKIES_ALWAYS = 1 143 | PF_SYNCOOKIES_ADAPTIVE = 2 144 | PF_SYNCOOKIES_MODE_MAX = PF_SYNCOOKIES_ADAPTIVE 145 | 146 | # Pool IDs (from /usr/src/sbin/pfctl/pfctl_parser.c) 147 | PF_POOL_ROUTE = 0 148 | PF_POOL_NAT = 1 149 | PF_POOL_RDR = 2 150 | 151 | # Pool options (from /usr/include/net/pfvar.h) 152 | PF_POOL_TYPEMASK = 0x0f 153 | PF_POOL_STICKYADDR = 0x20 154 | 155 | # Pool types (from /usr/include/net/pfvar.h) 156 | PF_POOL_NONE = 0 157 | PF_POOL_BITMASK = 1 158 | PF_POOL_RANDOM = 2 159 | PF_POOL_SRCHASH = 3 160 | PF_POOL_ROUNDROBIN = 4 161 | PF_POOL_LEASTSTATES = 5 162 | 163 | # Mask for window scaling factor (from /usr/include/net/pfvar.h) 164 | PF_WSCALE_MASK = 0x0f 165 | 166 | # Debug levels (from /usr/include/sys/syslog.h) 167 | LOG_EMERG = 0 168 | LOG_ALERT = 1 169 | LOG_CRIT = 2 170 | LOG_ERR = 3 171 | LOG_WARNING = 4 172 | LOG_NOTICE = 5 173 | LOG_INFO = 6 174 | LOG_DEBUG = 7 175 | 176 | # The 'unlimited' value for limits on the memory pools 177 | UINT_MAX = 0xffffffff 178 | 179 | # Limits (from /usr/include/net/pfvar.h) 180 | PF_LIMIT_STATES = 0 181 | PF_LIMIT_SRC_NODES = 1 182 | PF_LIMIT_FRAGS = 2 183 | PF_LIMIT_TABLES = 3 184 | PF_LIMIT_TABLE_ENTRIES = 4 185 | PF_LIMIT_PKTDELAY_PKTS = 5 186 | PF_LIMIT_MAX = 6 187 | 188 | # Timeouts (from /usr/include/net/pfvar.h) 189 | PFTM_TCP_FIRST_PACKET = 0 190 | PFTM_TCP_OPENING = 1 191 | PFTM_TCP_ESTABLISHED = 2 192 | PFTM_TCP_CLOSING = 3 193 | PFTM_TCP_FIN_WAIT = 4 194 | PFTM_TCP_CLOSED = 5 195 | PFTM_UDP_FIRST_PACKET = 6 196 | PFTM_UDP_SINGLE = 7 197 | PFTM_UDP_MULTIPLE = 8 198 | PFTM_ICMP_FIRST_PACKET = 9 199 | PFTM_ICMP_ERROR_REPLY = 10 200 | PFTM_OTHER_FIRST_PACKET = 11 201 | PFTM_OTHER_SINGLE = 12 202 | PFTM_OTHER_MULTIPLE = 13 203 | PFTM_FRAG = 14 204 | PFTM_INTERVAL = 15 205 | PFTM_ADAPTIVE_START = 16 206 | PFTM_ADAPTIVE_END = 17 207 | PFTM_SRC_NODE = 18 208 | PFTM_TS_DIFF = 19 209 | PFTM_MAX = 20 210 | PFTM_PURGE = 21 211 | PFTM_UNLINKED = 22 212 | 213 | # TCP States (from /usr/include/netinet/tcp_fsm.h) 214 | TCPS_CLOSED = 0 215 | TCPS_LISTEN = 1 216 | TCPS_SYN_SENT = 2 217 | TCPS_SYN_RECEIVED = 3 218 | TCPS_ESTABLISHED = 4 219 | TCPS_CLOSE_WAIT = 5 220 | TCPS_FIN_WAIT_1 = 6 221 | TCPS_CLOSING = 7 222 | TCPS_LAST_ACK = 8 223 | TCPS_FIN_WAIT_2 = 9 224 | TCPS_TIME_WAIT = 10 225 | TCP_NSTATES = 11 226 | 227 | # From /usr/include/net/pfvar.h 228 | PF_TCPS_PROXY_SRC = TCP_NSTATES + 0 229 | PF_TCPS_PROXY_DST = TCP_NSTATES + 1 230 | 231 | # UDP state enumeration (from /usr/include/net/pfvar.h) 232 | PFUDPS_NO_TRAFFIC = 0 233 | PFUDPS_SINGLE = 1 234 | PFUDPS_MULTIPLE = 2 235 | PFUDPS_NSTATES = 3 236 | 237 | # States for non-TCP protocols (from /usr/include/net/pfvar.h) 238 | PFOTHERS_NO_TRAFFIC = 0 239 | PFOTHERS_SINGLE = 1 240 | PFOTHERS_MULTIPLE = 2 241 | PFOTHERS_NSTATES = 3 242 | 243 | # Pfsync flags (from /usr/include/net/pfvar.h) 244 | PFSYNC_FLAG_SRCNODE = 0x04 245 | PFSYNC_FLAG_NATSRCNODE = 0x08 246 | 247 | # PF states flags (from /usr/include/net/pfvar.h) 248 | PFSTATE_ALLOWOPTS = 0x0001 249 | PFSTATE_SLOPPY = 0x0002 250 | PFSTATE_PFLOW = 0x0004 251 | PFSTATE_NOSYNC = 0x0008 252 | PFSTATE_ACK = 0x0010 253 | PFSTATE_NODF = 0x0020 254 | PFSTATE_SETTOS = 0x0040 255 | PFSTATE_RANDOMID = 0x0080 256 | PFSTATE_SCRUB_TCP = 0x0100 257 | PFSTATE_SETPRIO = 0x0200 258 | PFSTATE_SCRUBMASK = PFSTATE_NODF|PFSTATE_RANDOMID|PFSTATE_SCRUB_TCP 259 | PFSTATE_SETMASK = PFSTATE_SETTOS|PFSTATE_SETPRIO 260 | 261 | # Reassembly flags (from /usr/include/net/pfvar.h) 262 | PF_REASS_ENABLED = 0x01 263 | PF_REASS_NODF = 0x02 264 | 265 | # Table flags (from /usr/include/net/pfvar.h) 266 | PFR_TFLAG_PERSIST = 0x01 267 | PFR_TFLAG_CONST = 0x02 268 | PFR_TFLAG_ACTIVE = 0x04 269 | PFR_TFLAG_INACTIVE = 0x08 270 | PFR_TFLAG_REFERENCED = 0x10 271 | PFR_TFLAG_REFDANCHOR = 0x20 272 | PFR_TFLAG_COUNTERS = 0x40 273 | PFR_TFLAG_USRMASK = 0x43 274 | PFR_TFLAG_SETMASK = 0x3C 275 | PFR_TFLAG_ALLMASK = 0x7F 276 | 277 | PFR_FLAG_DUMMY = 0x00000002 278 | PFR_FLAG_FEEDBACK = 0x00000004 279 | PFR_FLAG_CLSTATS = 0x00000008 280 | PFR_FLAG_ADDRSTOO = 0x00000010 281 | PFR_FLAG_REPLACE = 0x00000020 282 | PFR_FLAG_ALLRSETS = 0x00000040 283 | PFR_FLAG_ALLMASK = 0x0000007f 284 | 285 | PFR_DIR_IN = 0 286 | PFR_DIR_OUT = 1 287 | PFR_DIR_MAX = 2 288 | 289 | PFR_OP_BLOCK = 0 290 | PFR_OP_MATCH = 1 291 | PFR_OP_PASS = 2 292 | PFR_OP_ADDR_MAX = 3 293 | PFR_OP_TABLE_MAX = 4 294 | PFR_OP_XPASS = PFR_OP_ADDR_MAX 295 | 296 | PFR_REFCNT_RULE = 0 297 | PFR_REFCNT_ANCHOR = 1 298 | PFR_REFCNT_MAX = 2 299 | 300 | # pfrke type (from /usr/include/net/pfvar.h) 301 | PFRKE_PLAIN = 0 302 | PFRKE_ROUTE = 1 303 | PFRKE_COST = 2 304 | PFRKE_MAX = 3 305 | 306 | # Interface flags (from /usr/include/net/pfvar.h) 307 | PFI_IFLAG_SKIP = 0x0100 308 | PFI_IFLAG_ANY = 0x0200 309 | 310 | # From /usr/src/sbin/pfctl/pfctl.h 311 | DEFAULT_PRIORITY = 1 312 | DEFAULT_QLIMIT = 50 313 | 314 | # Queue flags (from /usr/include/net/pfvar.h) 315 | PFQS_FLOWQUEUE = 0x0001 316 | PFQS_ROOTCLASS = 0x0002 317 | PFQS_DEFAULT = 0x1000 318 | 319 | # Match "prio 0" packets 320 | PF_PRIO_ZERO = 0xff 321 | 322 | # ICMP types (from /usr/include/netinet/ip_icmp.h) 323 | ICMP_ECHO = 8 324 | ICMP_ECHOREPLY = 0 325 | ICMP_UNREACH = 3 326 | ICMP_SOURCEQUENCH = 4 327 | ICMP_REDIRECT = 5 328 | ICMP_ALTHOSTADDR = 6 329 | ICMP_ROUTERADVERT = 9 330 | ICMP_ROUTERSOLICIT = 10 331 | ICMP_TIMXCEED = 11 332 | ICMP_PARAMPROB = 12 333 | ICMP_TSTAMP = 13 334 | ICMP_TSTAMPREPLY = 14 335 | ICMP_IREQ = 15 336 | ICMP_IREQREPLY = 16 337 | ICMP_MASKREQ = 17 338 | ICMP_MASKREPLY = 18 339 | ICMP_TRACEROUTE = 30 340 | ICMP_DATACONVERR = 31 341 | ICMP_MOBILE_REDIRECT = 32 342 | ICMP_IPV6_WHEREAREYOU = 33 343 | ICMP_IPV6_IAMHERE = 34 344 | ICMP_MOBILE_REGREQUEST = 35 345 | ICMP_MOBILE_REGREPLY = 36 346 | ICMP_SKIP = 39 347 | ICMP_PHOTURIS = 40 348 | 349 | # ICMP codes (from /usr/include/netinet/ip_icmp.h) 350 | ICMP_UNREACH_NET = 0 # Destination unreachable 351 | ICMP_UNREACH_HOST = 1 352 | ICMP_UNREACH_PROTOCOL = 2 353 | ICMP_UNREACH_PORT = 3 354 | ICMP_UNREACH_NEEDFRAG = 4 355 | ICMP_UNREACH_SRCFAIL = 5 356 | ICMP_UNREACH_NET_UNKNOWN = 6 357 | ICMP_UNREACH_HOST_UNKNOWN = 7 358 | ICMP_UNREACH_ISOLATED = 8 359 | ICMP_UNREACH_NET_PROHIB = 9 360 | ICMP_UNREACH_HOST_PROHIB = 10 361 | ICMP_UNREACH_TOSNET = 11 362 | ICMP_UNREACH_TOSHOST = 12 363 | ICMP_UNREACH_FILTER_PROHIB = 13 364 | ICMP_UNREACH_HOST_PRECEDENCE = 14 365 | ICMP_UNREACH_PRECEDENCE_CUTOFF = 15 366 | ICMP_REDIRECT_NET = 0 # Shorter route 367 | ICMP_REDIRECT_HOST = 1 368 | ICMP_REDIRECT_TOSNET = 2 369 | ICMP_REDIRECT_TOSHOST = 3 370 | ICMP_ROUTERADVERT_NORMAL = 0 # Router advertisement 371 | ICMP_ROUTERADVERT_NOROUTE_COMMON = 16 372 | ICMP_TIMXCEED_INTRANS = 0 # Time exceeded 373 | ICMP_TIMXCEED_REASS = 1 374 | ICMP_PARAMPROB_ERRATPTR = 0 # IP header bad 375 | ICMP_PARAMPROB_OPTABSENT = 1 376 | ICMP_PARAMPROB_LENGTH = 2 377 | ICMP_PHOTURIS_UNKNOWN_INDEX = 1 # Photuris 378 | ICMP_PHOTURIS_AUTH_FAILED = 2 379 | ICMP_PHOTURIS_DECRYPT_FAILED = 3 380 | 381 | # ICMP6 types (from /usr/include/netinet/icmp6.h) 382 | ICMP6_DST_UNREACH = 1 383 | ICMP6_PACKET_TOO_BIG = 2 384 | ICMP6_TIME_EXCEEDED = 3 385 | ICMP6_PARAM_PROB = 4 386 | ICMP6_ECHO_REQUEST = 128 387 | ICMP6_ECHO_REPLY = 129 388 | ICMP6_MEMBERSHIP_QUERY = 130 389 | MLD_LISTENER_QUERY = 130 390 | ICMP6_MEMBERSHIP_REPORT = 131 391 | MLD_LISTENER_REPORT = 131 392 | ICMP6_MEMBERSHIP_REDUCTION = 132 393 | MLD_LISTENER_DONE = 132 394 | ND_ROUTER_SOLICIT = 133 395 | ND_ROUTER_ADVERT = 134 396 | ND_NEIGHBOR_SOLICIT = 135 397 | ND_NEIGHBOR_ADVERT = 136 398 | ND_REDIRECT = 137 399 | ICMP6_ROUTER_RENUMBERING = 138 400 | ICMP6_WRUREQUEST = 139 401 | ICMP6_WRUREPLY = 140 402 | ICMP6_FQDN_QUERY = 139 403 | ICMP6_FQDN_REPLY = 140 404 | ICMP6_NI_QUERY = 139 405 | ICMP6_NI_REPLY = 140 406 | MLD_MTRACE_RESP = 200 407 | MLD_MTRACE = 201 408 | 409 | # ICMP6 codes (from /usr/include/netinet/icmp6.h) 410 | ICMP6_DST_UNREACH_NOROUTE = 0 411 | ICMP6_DST_UNREACH_ADMIN = 1 412 | ICMP6_DST_UNREACH_NOTNEIGHBOR = 2 413 | ICMP6_DST_UNREACH_BEYONDSCOPE = 2 414 | ICMP6_DST_UNREACH_ADDR = 3 415 | ICMP6_DST_UNREACH_NOPORT = 4 416 | ICMP6_TIME_EXCEED_TRANSIT = 0 417 | ICMP6_TIME_EXCEED_REASSEMBLY = 1 418 | ICMP6_PARAMPROB_HEADER = 0 419 | ICMP6_PARAMPROB_NEXTHEADER = 1 420 | ICMP6_PARAMPROB_OPTION = 2 421 | ND_REDIRECT_ONLINK = 0 422 | ND_REDIRECT_ROUTER = 1 423 | -------------------------------------------------------------------------------- /pf/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for PF-related errors""" 2 | 3 | 4 | class PFError(Exception): 5 | """Base class for PF-related errors.""" 6 | pass 7 | -------------------------------------------------------------------------------- /pf/filter.py: -------------------------------------------------------------------------------- 1 | """A class for managing OpenBSD's Packet Filter. 2 | 3 | This class communicates with the kernel through the ioctl(2) interface provided 4 | by the pf(4) pseudo-device; this allows Python to natively send commands to the 5 | kernel, thanks to the fcntl and ctypes modules. 6 | """ 7 | 8 | import os 9 | import stat 10 | from fcntl import ioctl 11 | from errno import * 12 | from ctypes import * 13 | from socket import * 14 | 15 | from pf.exceptions import PFError 16 | from pf.constants import * 17 | from pf._struct import * 18 | from pf._base import PFObject 19 | from pf.queue import * 20 | from pf.state import PFState 21 | from pf.status import PFStatus, PFIface 22 | from pf.table import PFTableAddr, PFTable, PFTStats 23 | from pf.rule import PFRule, PFRuleset, pf_timeouts 24 | from pf._utils import * 25 | 26 | 27 | __all__ = ['PacketFilter'] 28 | 29 | 30 | # ioctl() operations 31 | IOCPARM_MASK = 0x1fff 32 | IOC_VOID = 0x20000000 33 | IOC_OUT = 0x40000000 34 | IOC_IN = 0x80000000 35 | IOC_INOUT = IOC_IN | IOC_OUT 36 | 37 | def _IOC(inout, group, num, len): 38 | return (inout | ((len & IOCPARM_MASK) << 16) | ((group) << 8) | (num)) 39 | 40 | def _IO(group, num): 41 | return _IOC(IOC_VOID, ord(group), num, 0) 42 | 43 | def _IOWR(group, num, type): 44 | return _IOC(IOC_INOUT, ord(group), num, sizeof(type)) 45 | 46 | DIOCSTART = _IO ('D', 1) 47 | DIOCSTOP = _IO ('D', 2) 48 | DIOCADDRULE = _IOWR('D', 4, pfioc_rule) 49 | DIOCGETRULES = _IOWR('D', 6, pfioc_rule) 50 | DIOCGETRULE = _IOWR('D', 7, pfioc_rule) 51 | DIOCCLRSTATES = _IOWR('D', 18, pfioc_state_kill) 52 | #DIOCGETSTATE = _IOWR('D', 19, pfioc_state) 53 | DIOCSETSTATUSIF = _IOWR('D', 20, pfioc_iface) 54 | DIOCGETSTATUS = _IOWR('D', 21, pf_status) 55 | DIOCCLRSTATUS = _IOWR('D', 22, pfioc_iface) 56 | #DIOCNATLOOK = _IOWR('D', 23, pfioc_natlook) 57 | DIOCSETDEBUG = _IOWR('D', 24, c_uint32) 58 | DIOCGETSTATES = _IOWR('D', 25, pfioc_states) 59 | #DIOCCHANGERULE = _IOWR('D', 26, pfioc_rule) 60 | DIOCSETTIMEOUT = _IOWR('D', 29, pfioc_tm) 61 | DIOCGETTIMEOUT = _IOWR('D', 30, pfioc_tm) 62 | #DIOCADDSTATE = _IOWR('D', 37, pfioc_state) 63 | #DIOCCLRRULECTRS = _IO ('D', 38) 64 | DIOCGETLIMIT = _IOWR('D', 39, pfioc_limit) 65 | DIOCSETLIMIT = _IOWR('D', 40, pfioc_limit) 66 | DIOCKILLSTATES = _IOWR('D', 41, pfioc_state_kill) 67 | #DIOCGETRULESETS = _IOWR('D', 58, pfioc_ruleset) 68 | #DIOCGETRULESET = _IOWR('D', 59, pfioc_ruleset) 69 | DIOCRCLRTABLES = _IOWR('D', 60, pfioc_table) 70 | DIOCRADDTABLES = _IOWR('D', 61, pfioc_table) 71 | DIOCRDELTABLES = _IOWR('D', 62, pfioc_table) 72 | DIOCRGETTABLES = _IOWR('D', 63, pfioc_table) 73 | DIOCRGETTSTATS = _IOWR('D', 64, pfioc_table) 74 | DIOCRCLRTSTATS = _IOWR('D', 65, pfioc_table) 75 | DIOCRCLRADDRS = _IOWR('D', 66, pfioc_table) 76 | DIOCRADDADDRS = _IOWR('D', 67, pfioc_table) 77 | DIOCRDELADDRS = _IOWR('D', 68, pfioc_table) 78 | DIOCRSETADDRS = _IOWR('D', 69, pfioc_table) 79 | DIOCRGETADDRS = _IOWR('D', 70, pfioc_table) 80 | #DIOCRGETASTATS = _IOWR('D', 71, pfioc_table) 81 | #DIOCRCLRASTATS = _IOWR('D', 72, pfioc_table) 82 | DIOCRTSTADDRS = _IOWR('D', 73, pfioc_table) 83 | #DIOCRSETTFLAGS = _IOWR('D', 74, pfioc_table) 84 | DIOCRINADEFINE = _IOWR('D', 77, pfioc_table) 85 | #DIOCOSFPFLUSH = _IO ('D', 78) 86 | #DIOCOSFPADD = _IOWR('D', 79, pf_osfp_ioctl) 87 | #DIOCOSFPGET = _IOWR('D', 80, pf_osfp_ioctl) 88 | DIOCXBEGIN = _IOWR('D', 81, pfioc_trans) 89 | DIOCXCOMMIT = _IOWR('D', 82, pfioc_trans) 90 | DIOCXROLLBACK = _IOWR('D', 83, pfioc_trans) 91 | #DIOCGETSRCNODES = _IOWR('D', 84, pfioc_src_nodes) 92 | #DIOCCLRSRCNODES = _IO ('D', 85) 93 | DIOCSETHOSTID = _IOWR('D', 86, c_uint32) 94 | DIOCIGETIFACES = _IOWR('D', 87, pfioc_iface) 95 | DIOCSETIFFLAG = _IOWR('D', 89, pfioc_iface) 96 | DIOCCLRIFFLAG = _IOWR('D', 90, pfioc_iface) 97 | #DIOCKILLSRCNODES = _IOWR('D', 91, pfioc_src_node_kill) 98 | DIOCSETREASS = _IOWR('D', 92, c_uint32) 99 | DIOCADDQUEUE = _IOWR('D', 93, pfioc_queue) 100 | DIOCGETQUEUES = _IOWR('D', 94, pfioc_queue) 101 | DIOCGETQUEUE = _IOWR('D', 95, pfioc_queue) 102 | DIOCGETQSTATS = _IOWR('D', 96, pfioc_qstats) 103 | DIOCSETSYNFLWATS = _IOWR('D', 97, pfioc_synflwats) 104 | DIOCSETSYNCOOKIES = _IOWR('D', 98, c_uint8) 105 | DIOCGETSYNFLWATS = _IOWR('D', 99, pfioc_synflwats) 106 | 107 | 108 | 109 | class _PFTrans(object): 110 | """Class for managing transactions with the Packet Filter subsystem.""" 111 | 112 | def __init__(self, dev, path="", *trans_type): 113 | """Initialize the required structures.""" 114 | self.dev = dev 115 | self.size = len(trans_type) 116 | self.array = (pfioc_trans_e * self.size)() 117 | 118 | for a, t in zip(self.array, trans_type): 119 | a.type = t 120 | a.anchor = path.encode() 121 | 122 | self._pt = pfioc_trans(size=self.size, esize=sizeof(pfioc_trans_e), 123 | array=addressof(self.array)) 124 | 125 | def __enter__(self): 126 | """Start the transaction.""" 127 | ioctl(self.dev, DIOCXBEGIN, self._pt) 128 | return self 129 | 130 | def __exit__(self, type, value, traceback): 131 | """Commit changes if no exceptions occurred; otherwise, rollback.""" 132 | if type is None: 133 | ioctl(self.dev, DIOCXCOMMIT, self._pt) 134 | else: 135 | ioctl(self.dev, DIOCXROLLBACK, self._pt) 136 | 137 | 138 | class PacketFilter(object): 139 | """Class representing the kernel's packet filtering subsystem. 140 | 141 | It provides a set of methods that allow you to send commands to the kernel 142 | through the ioctl(2) interface provided by the pf(4) pseudo-device. 143 | Basically, all methods in this class are just wrappers to ioctl(2) calls, 144 | and may consequently raise IOError if the ioctl() request fails. 145 | """ 146 | 147 | def __init__(self, dev="/dev/pf"): 148 | """Set the pf device.""" 149 | self.dev = dev 150 | 151 | def enable(self): 152 | """Enable Packet Filtering.""" 153 | with open(self.dev, 'w') as d: 154 | try: 155 | ioctl(d, DIOCSTART) 156 | except IOError as e: 157 | if e != EEXIST: # EEXIST means PF is already enabled 158 | raise 159 | 160 | def disable(self): 161 | """Disable Packet Filtering.""" 162 | with open(self.dev, 'w') as d: 163 | try: 164 | ioctl(d, DIOCSTOP) 165 | except IOError as e: 166 | if e != ENOENT: # ENOENT means PF is already disabled 167 | raise 168 | 169 | def set_debug(self, level): 170 | """Set the debug level. 171 | 172 | The debug level can be either one of the LOG_* constants or a string. 173 | """ 174 | if level in dbg_levels: 175 | level = dbg_levels[level] 176 | 177 | with open(self.dev, 'w') as d: 178 | with _PFTrans(d): 179 | ioctl(d, DIOCSETDEBUG, c_uint32(level)) 180 | 181 | def set_hostid(self, id): 182 | """Set the host ID. 183 | 184 | The host ID is used by pfsync to identify the host that created a state 185 | table entry. 'id' must be a 32-bit unsigned integer. 186 | """ 187 | with open(self.dev, 'w') as d: 188 | with _PFTrans(d): 189 | ioctl(d, DIOCSETHOSTID, c_uint32(htonl(id))) 190 | 191 | def set_reassembly(self, reassembly): 192 | """Enable reassembly of network traffic. 193 | 194 | The 'reassembly' argument specifies the flags for the reassembly 195 | operation; available flags are PF_REASS_ENABLED and PF_REASS_NODF. 196 | """ 197 | with open(self.dev, 'w') as d: 198 | with _PFTrans(d): 199 | ioctl(d, DIOCSETREASS, c_uint32(reassembly)) 200 | 201 | def get_limit(self, limit=None): 202 | """Return the hard limits on the memory pools used by Packet Filter. 203 | 204 | 'limit' can be either one of the PF_LIMIT_* constants or a string; 205 | return the value of the requested limit (UINT_MAX means unlimited) or, 206 | if called with no arguments, a dictionary containing all the available 207 | limits. 208 | """ 209 | if limit is None: 210 | return dict([(l, self.get_limit(l)) for l in pf_limits]) 211 | elif limit in pf_limits: 212 | limit = pf_limits[limit] 213 | 214 | pl = pfioc_limit(index=limit) 215 | 216 | with open(self.dev, 'r') as d: 217 | ioctl(d, DIOCGETLIMIT, pl) 218 | 219 | return pl.limit 220 | 221 | def set_limit(self, limit, value): 222 | """Set hard limits on the memory pools used by Packet Filter. 223 | 224 | 'limit' can be either one of the PF_LIMIT_* constants or a string; a 225 | 'value' of UINT_MAX means unlimited. Raise PFError if the current pool 226 | size exceeds the requested hard limit. 227 | """ 228 | if limit in pf_limits: 229 | limit = pf_limits[limit] 230 | 231 | pl = pfioc_limit(index=limit, limit=value) 232 | with open(self.dev, 'w') as d: 233 | with _PFTrans(d): 234 | try: 235 | ioctl(d, DIOCSETLIMIT, pl) 236 | except IOError as e: 237 | if e == EBUSY: 238 | raise PFError("Current pool size > {0:d}".format(value)) 239 | raise 240 | 241 | def get_timeout(self, timeout=None): 242 | """Return the configured timeout values for PF states. 243 | 244 | 'timeout' can be either one of the PFTM_* constants or a string; return 245 | the value of the requested timeout or, if called with no arguments, a 246 | dictionary containing all the available timeouts. 247 | """ 248 | if timeout is None: 249 | return dict([(t, self.get_timeout(t)) for t in pf_timeouts]) 250 | elif timeout in pf_timeouts: 251 | timeout = pf_timeouts[timeout] 252 | 253 | tm = pfioc_tm(timeout=timeout) 254 | with open(self.dev, 'r') as d: 255 | ioctl(d, DIOCGETTIMEOUT, tm) 256 | 257 | return tm.seconds 258 | 259 | def set_timeout(self, timeout, value): 260 | """Set the timeout 'value' for a specific PF state. 261 | 262 | 'timeout' can be either one of the PFTM_* constants or a string; return 263 | the old value of the specified timeout. 264 | """ 265 | if timeout in pf_timeouts: 266 | timeout = pf_timeouts[timeout] 267 | 268 | tm = pfioc_tm(timeout=timeout, seconds=value) 269 | with open(self.dev, 'w') as d: 270 | with _PFTrans(d): 271 | ioctl(d, DIOCSETTIMEOUT, tm) 272 | 273 | return tm.seconds 274 | 275 | def set_optimization(self, opt="normal"): 276 | """Set the optimization profile for state handling like pfctl.""" 277 | for name, val in pf_hints[opt].items(): 278 | self.set_timeout(name, val) 279 | 280 | def get_optimization(self): 281 | """ """ 282 | tm = self.get_timeout() 283 | for name, val in pf_hints.items(): 284 | if val["tcp.first"] == tm["tcp.first"]: 285 | return name 286 | 287 | def get_ifaces(self, ifname="", size=8): 288 | """Get the list of interfaces and interface drivers known to pf. 289 | 290 | Return a tuple of PFIface objects or a single PFIface object if a 291 | specific 'ifname' is specified. 292 | """ 293 | pi = pfioc_iface(pfiio_name=ifname.encode(), 294 | pfiio_esize=sizeof(pfi_kif), 295 | pfiio_size=size+1) 296 | 297 | with open(self.dev, 'w') as d: 298 | buf = (pfi_kif * pi.pfiio_size)() 299 | pi.pfiio_buffer = addressof(buf) 300 | ioctl(d, DIOCIGETIFACES, pi) 301 | 302 | if ifname: 303 | return PFIface(buf[0]) 304 | else: 305 | ifaces = tuple(i for i in map(PFIface, buf) if i.name) 306 | if len(ifaces) == size: 307 | return self.get_ifaces(ifname, size*2) 308 | return ifaces 309 | 310 | def set_ifflags(self, ifname, flags): 311 | """Set the user setable 'flags' on the interface 'ifname'.""" 312 | pi = pfioc_iface(pfiio_name=ifname.encode(), pfiio_flags=flags) 313 | with open(self.dev, 'w') as d: 314 | with _PFTrans(d): 315 | ioctl(d, DIOCSETIFFLAG, pi) 316 | 317 | def clear_ifflags(self, ifname, flags=None): 318 | """Clear the specified user setable 'flags' on the interface 'ifname'. 319 | 320 | If no flags are specified, clear all flags. 321 | """ 322 | if flags is None: 323 | flags = PFI_IFLAG_SKIP 324 | 325 | pi = pfioc_iface(pfiio_name=ifname.encode(), pfiio_flags=flags) 326 | with open(self.dev, 'w') as d: 327 | with _PFTrans(d): 328 | ioctl(d, DIOCCLRIFFLAG, pi) 329 | 330 | def set_status_if(self, ifname=""): 331 | """Specify the interface for which statistics are accumulated. 332 | 333 | If no 'ifname' is provided, turn off the collection of per-interface 334 | statistics. Raise PFError if 'ifname' is not a valid interface name. 335 | """ 336 | pi = pfioc_iface(pfiio_name=ifname.encode()) 337 | with open(self.dev, 'w') as d: 338 | with _PFTrans(d): 339 | try: 340 | ioctl(d, DIOCSETSTATUSIF, pi) 341 | except IOError as e: 342 | if e == EINVAL: 343 | raise PFError("Invalid ifname: '{0}'".format(ifname)) 344 | raise 345 | 346 | def get_status(self): 347 | """Return a PFStatus object containing the internal PF statistics.""" 348 | s = pf_status() 349 | with open(self.dev, 'w') as d: 350 | ioctl(d, DIOCGETSTATUS, s) 351 | 352 | return PFStatus(s) 353 | 354 | def clear_status(self, ifname=""): 355 | """Clear the internal packet filter statistics. 356 | 357 | An optional 'ifname' can be specified in order to clear statistics only 358 | for a specific interface. 359 | """ 360 | pi = pfioc_iface(pfiio_name=ifname.encode()) 361 | with open(self.dev, 'w') as d: 362 | ioctl(d, DIOCCLRSTATUS, pi) 363 | 364 | def get_states(self): 365 | """Retrieve Packet Filter's state table entries. 366 | 367 | Return a tuple of PFState objects representing the states currently 368 | tracked by PF. 369 | """ 370 | ps = pfioc_states() 371 | 372 | l = 0 373 | with open(self.dev, 'w') as d: 374 | while True: 375 | if l: 376 | ps_states = (pfsync_state * int(l / sizeof(pfsync_state)))() 377 | ps.ps_buf = addressof(ps_states) 378 | ps.ps_len = l 379 | ioctl(d, DIOCGETSTATES, ps) 380 | if ps.ps_len == 0: 381 | return () 382 | if ps.ps_len <= l: 383 | break 384 | l = (ps.ps_len * 2) 385 | 386 | ps_num = int(ps.ps_len / sizeof(pfsync_state)) 387 | return tuple([PFState(s) for s in ps_states[:ps_num]]) 388 | 389 | def clear_states(self, ifname=""): 390 | """Clear all states. 391 | 392 | If an interface name is provided, only states for that interface will 393 | be cleared. Return the number of cleared states. 394 | """ 395 | psk = pfioc_state_kill(psk_ifname=ifname) 396 | 397 | with open(self.dev, 'w') as d: 398 | ioctl(d, DIOCCLRSTATES, psk) 399 | 400 | return psk.psk_killed 401 | 402 | def kill_states(self, af=AF_UNSPEC, proto=0, src=None, dst=None, ifname="", 403 | label="", rdomain=0): 404 | """Clear states matching the specified arguments. 405 | 406 | States can be specified by address family, layer-4 protocol, source and 407 | destination addresses, interface name, label and routing domain. Return 408 | the number of killed states. 409 | """ 410 | psk = pfioc_state_kill(psk_af=af, psk_proto=proto, psk_ifname=ifname, 411 | psk_label=label, psk_rdomain=rdomain) 412 | if src: 413 | psk.psk_src = src._to_struct() 414 | if dst: 415 | psk.psk_dst = dst._to_struct() 416 | 417 | with open(self.dev, 'w') as d: 418 | ioctl(d, DIOCKILLSTATES, psk) 419 | 420 | return psk.psk_killed 421 | 422 | def clear_rules(self, path=""): 423 | """Clear all rules contained in the anchor 'path'.""" 424 | self.load_ruleset(PFRuleset(), path, PF_TRANS_RULESET) 425 | 426 | def load_queues(self, *queues): 427 | """Load a set of queues on an interface. 428 | 429 | 'queues' must be PFQueue objects. 430 | """ 431 | with open(self.dev, 'w') as d: 432 | with _PFTrans(d, "", PF_TRANS_RULESET) as t: 433 | for queue in queues: 434 | q = pfioc_queue(ticket=t.array[0].ticket, 435 | queue=queue._to_struct()) 436 | ioctl(d, DIOCADDQUEUE, q) 437 | 438 | def get_queues(self): 439 | """Retrieve the currently loaded queues. 440 | 441 | Return a tuple of PFQueue objects. 442 | """ 443 | queues = [] 444 | pq = pfioc_queue() 445 | with open(self.dev, 'r') as d: 446 | ioctl(d, DIOCGETQUEUES, pq) 447 | 448 | qstats = queue_stats() 449 | for nr in range(pq.nr): 450 | pqs = pfioc_qstats(nr=nr, ticket=pq.ticket, 451 | buf=addressof(qstats.data), 452 | nbytes=sizeof(hfsc_class_stats)) 453 | ioctl(d, DIOCGETQSTATS, pqs) 454 | queue = PFQueue(pqs.queue) 455 | queue.stats = PFQueueStats(qstats.data) 456 | queues.append(queue) 457 | 458 | return queues 459 | 460 | def _get_rules(self, path, dev, clear): 461 | """Recursively retrieve rules from the specified ruleset.""" 462 | if path.endswith("/*"): 463 | path = path[:-2] 464 | 465 | pr = pfioc_rule(anchor=path.encode()) 466 | if clear: 467 | pr.action = PF_GET_CLR_CNTR 468 | 469 | pr.rule.action = PF_PASS 470 | ioctl(dev, DIOCGETRULES, pr) 471 | 472 | tables = list(self.get_tables(PFTable(anchor=path))) 473 | rules = [] 474 | for nr in range(pr.nr): 475 | pr.nr = nr 476 | ioctl(dev, DIOCGETRULE, pr) 477 | if pr.anchor_call: 478 | path = os.path.join(pr.anchor.decode(), pr.anchor_call.decode()) 479 | rs = PFRuleset(pr.anchor_call.decode(), pr.rule) 480 | rs.append(*self._get_rules(path, dev, clear)) 481 | rules.append(rs) 482 | else: 483 | rules.append(PFRule(pr.rule)) 484 | 485 | return tables + rules 486 | 487 | def get_ruleset(self, path="", clear=False, **kw): 488 | """Return a PFRuleset object containing the active ruleset. 489 | 490 | 'path' is the path of the anchor to retrieve rules from. If 'clear' is 491 | True, per-rule statistics will be cleared. Keyword arguments can be 492 | passed for returning only matching rules. 493 | """ 494 | rs = PFRuleset(os.path.basename(path)) 495 | 496 | with open(self.dev, 'r') as d: 497 | for rule in self._get_rules(path, d, clear): 498 | if isinstance(rule, PFRule): 499 | if not all((getattr(rule, attr) == value) 500 | for (attr, value) in kw.items()): 501 | continue 502 | rs.append(rule) 503 | return rs 504 | 505 | def _inadefine(self, table, dev, path, ticket): 506 | """Define a table in the inactive ruleset.""" 507 | table.anchor = path 508 | io = pfioc_table(pfrio_table=table._to_struct(), pfrio_ticket=ticket, 509 | pfrio_esize=sizeof(pfr_addr)) 510 | 511 | if table.addrs: 512 | io.pfrio_flags |= PFR_FLAG_ADDRSTOO 513 | addrs = table.addrs 514 | buf = (pfr_addr * len(addrs))(*[a._to_struct() for a in addrs]) 515 | io.pfrio_buffer = addressof(buf) 516 | io.pfrio_size = len(addrs) 517 | 518 | ioctl(dev, DIOCRINADEFINE, io) 519 | 520 | def load_ruleset(self, ruleset, path="", *tr_type): 521 | """Load the given ruleset. 522 | 523 | 'ruleset' must be a PFRuleset object; 'path' is the name of the anchor 524 | where to load rules; 'tr_type' is one or more PF_TRANS_* constants: if 525 | omitted, all ruleset types will be loaded. 526 | """ 527 | if not tr_type: 528 | tr_type = (PF_TRANS_TABLE, PF_TRANS_RULESET) 529 | 530 | with open(self.dev, 'w') as d: 531 | with _PFTrans(d, path, *tr_type) as t: 532 | for a in t.array: 533 | if a.type == PF_TRANS_TABLE: 534 | for t in ruleset.tables: 535 | self._inadefine(t, d, path, a.ticket) 536 | elif a.type == PF_TRANS_RULESET: 537 | for r in ruleset.rules: 538 | pr = pfioc_rule(ticket=a.ticket, 539 | anchor=path.encode(), 540 | rule=r._to_struct()) 541 | 542 | if isinstance(r, PFRuleset): 543 | pr.anchor_call = r.name.encode() 544 | 545 | ioctl(d, DIOCADDRULE, pr) 546 | 547 | if isinstance(r, PFRuleset): 548 | self.load_ruleset(r, os.path.join(path, r.name), 549 | *tr_type) 550 | 551 | def add_tables(self, *tables): 552 | """Create one or more tables. 553 | 554 | 'tables' must be PFTable objects; return the number of tables created. 555 | """ 556 | io = pfioc_table(pfrio_esize=sizeof(pfr_table), pfrio_size=len(tables)) 557 | 558 | buffer = (pfr_table * len(tables))(*[t._to_struct() for t in tables]) 559 | io.pfrio_buffer = addressof(buffer) 560 | 561 | with open(self.dev, 'w') as d: 562 | ioctl(d, DIOCRADDTABLES, io) 563 | 564 | for t in filter(lambda t: t.addrs, tables): 565 | self.add_addrs(t, *t.addrs) 566 | 567 | return io.pfrio_nadd 568 | 569 | def clear_tables(self, filter=None): 570 | """Clear all tables. 571 | 572 | 'filter' is a PFTable object that allows you to specify the anchor of 573 | the tables to delete. Return the number of tables deleted. 574 | """ 575 | io = pfioc_table() 576 | 577 | if filter is not None: 578 | io.pfrio_table = pfr_table(pfrt_anchor=filter.anchor) 579 | 580 | with open(self.dev, 'w') as d: 581 | ioctl(d, DIOCRCLRTABLES, io) 582 | 583 | return io.pfrio_ndel 584 | 585 | def del_tables(self, *tables): 586 | """Delete one or more tables. 587 | 588 | 'tables' must be PFTable objects. Return the number of tables deleted. 589 | """ 590 | io = pfioc_table(pfrio_esize=sizeof(pfr_table), pfrio_size=len(tables)) 591 | 592 | buffer = (pfr_table * len(tables))() 593 | for (t, b) in zip(tables, buffer): 594 | b.pfrt_name = t.name.encode() 595 | b.pfrt_anchor = t.anchor.encode() 596 | 597 | io.pfrio_buffer = addressof(buffer) 598 | 599 | with open(self.dev, 'w') as d: 600 | ioctl(d, DIOCRDELTABLES, io) 601 | 602 | return io.pfrio_ndel 603 | 604 | def get_tables(self, filter=None, buf_size=10): 605 | """Get the list of all tables. 606 | 607 | 'filter' is a PFTable object that allows you to specify the anchor of 608 | the tables to retrieve. Return a tuple of PFTable objects containing 609 | the currently-loaded tables. 610 | """ 611 | io = pfioc_table(pfrio_esize=sizeof(pfr_table)) 612 | 613 | if filter is not None: 614 | io.pfrio_table = pfr_table(pfrt_anchor=filter.anchor.encode()) 615 | 616 | with open(self.dev, 'w') as d: 617 | while True: 618 | buffer = (pfr_table * buf_size)() 619 | io.pfrio_buffer = addressof(buffer) 620 | io.pfrio_size = buf_size 621 | 622 | ioctl(d, DIOCRGETTABLES, io) 623 | 624 | if io.pfrio_size <= buf_size: 625 | break 626 | buf_size = io.pfrio_size 627 | 628 | tables = [] 629 | for t in buffer[:io.pfrio_size]: 630 | try: 631 | addrs = self.get_addrs(PFTable(t)) 632 | except IOError as e: 633 | pass # Ignore tables of which you can't get the addresses 634 | else: 635 | tables.append(PFTable(t, *addrs)) 636 | 637 | return tuple(tables) 638 | 639 | def test_addrs(self, table, *addrs): 640 | """Test if one or more addresses match a table. 641 | 642 | 'table' can be either a PFTable instance or a string containing the 643 | table name; 'addrs' can be either PFTableAddr instances or strings. 644 | Return the addresses that match. 645 | """ 646 | if isinstance(table, str): 647 | table = pfr_table(pfrt_name=table.encode()) 648 | else: 649 | table = pfr_table(pfrt_name=table.name.encode(), 650 | pfrt_anchor=table.anchor.encode()) 651 | 652 | _addrs = [] 653 | for addr in addrs: 654 | if isinstance(addr, PFTableAddr): 655 | _addrs.append(addr) 656 | else: 657 | _addrs.append(PFTableAddr(addr)) 658 | 659 | io = pfioc_table(pfrio_table=table, pfrio_esize=sizeof(pfr_addr), 660 | pfrio_size=len(addrs)) 661 | 662 | buffer = (pfr_addr * len(addrs))(*[a._to_struct() for a in _addrs]) 663 | io.pfrio_buffer = addressof(buffer) 664 | 665 | with open(self.dev, 'w') as d: 666 | ioctl(d, DIOCRTSTADDRS, io) 667 | 668 | return tuple([PFTableAddr(a) for a in buffer[:io.pfrio_size] if a.pfra_fback]) 669 | 670 | def add_addrs(self, table, *addrs): 671 | """Add one or more addresses to a table. 672 | 673 | 'table' can be either a PFTable instance or a string containing the 674 | table name; 'addrs' can be either PFTableAddr instances or strings. 675 | Return the number of addresses effectively added. 676 | """ 677 | if isinstance(table, str): 678 | table = pfr_table(pfrt_name=table.encode()) 679 | else: 680 | table = pfr_table(pfrt_name=table.name.encode(), 681 | pfrt_anchor=table.anchor.encode()) 682 | 683 | _addrs = [] 684 | for addr in addrs: 685 | if isinstance(addr, PFTableAddr): 686 | _addrs.append(addr) 687 | else: 688 | _addrs.append(PFTableAddr(addr)) 689 | 690 | io = pfioc_table(pfrio_table=table, pfrio_esize=sizeof(pfr_addr), 691 | pfrio_size=len(addrs)) 692 | 693 | buffer = (pfr_addr * len(addrs))(*[a._to_struct() for a in _addrs]) 694 | io.pfrio_buffer = addressof(buffer) 695 | 696 | with open(self.dev, 'w') as d: 697 | ioctl(d, DIOCRADDADDRS, io) 698 | 699 | return io.pfrio_nadd 700 | 701 | def clear_addrs(self, table): 702 | """Clear all addresses in the specified table. 703 | 704 | Return the number of addresses removed. 705 | """ 706 | if isinstance(table, str): 707 | table = pfr_table(pfrt_name=table) 708 | else: 709 | table = pfr_table(pfrt_name=table.name.encode(), 710 | pfrt_anchor=table.anchor.encode()) 711 | 712 | io = pfioc_table(pfrio_table=table) 713 | 714 | with open(self.dev, 'w') as d: 715 | ioctl(d, DIOCRCLRADDRS, io) 716 | 717 | return io.pfrio_ndel 718 | 719 | def del_addrs(self, table, *addrs): 720 | """Delete one or more addresses from the specified table. 721 | 722 | 'table' can be either a PFTable instance or a string containing the 723 | table name; 'addrs' can be either PFTableAddr instances or strings. 724 | Return the number of addresses deleted. 725 | """ 726 | if isinstance(table, str): 727 | table = pfr_table(pfrt_name=table) 728 | else: 729 | table = pfr_table(pfrt_name=table.name.encode(), 730 | pfrt_anchor=table.anchor.encode()) 731 | 732 | _addrs = [] 733 | for addr in addrs: 734 | if isinstance(addr, PFTableAddr): 735 | _addrs.append(addr) 736 | else: 737 | _addrs.append(PFTableAddr(addr)) 738 | 739 | io = pfioc_table(pfrio_table=table, pfrio_esize=sizeof(pfr_addr), 740 | pfrio_size=len(addrs)) 741 | 742 | buffer = (pfr_addr * len(addrs))(*[a._to_struct() for a in _addrs]) 743 | io.pfrio_buffer = addressof(buffer) 744 | 745 | with open(self.dev, 'w') as d: 746 | ioctl(d, DIOCRDELADDRS, io) 747 | 748 | return io.pfrio_ndel 749 | 750 | def set_addrs(self, table, *addrs): 751 | """Replace the content of a table. 752 | 753 | 'table' can be either a PFTable instance or a string containing the 754 | table name; 'addrs' can be either PFTableAddr instances or strings. 755 | Return a tuple containing the number of addresses deleted, added and 756 | changed. 757 | """ 758 | if isinstance(table, str): 759 | table = pfr_table(pfrt_name=table) 760 | else: 761 | table = pfr_table(pfrt_name=table.name.encode(), 762 | pfrt_anchor=table.anchor.encode()) 763 | 764 | _addrs = [] 765 | for addr in addrs: 766 | if isinstance(addr, PFTableAddr): 767 | _addrs.append(addr) 768 | else: 769 | _addrs.append(PFTableAddr(addr)) 770 | 771 | io = pfioc_table(pfrio_table=table, pfrio_esize=sizeof(pfr_addr), 772 | pfrio_size=len(addrs)) 773 | 774 | buffer = (pfr_addr * len(addrs))(*[a._to_struct() for a in _addrs]) 775 | io.pfrio_buffer = addressof(buffer) 776 | 777 | with open(self.dev, 'w') as d: 778 | ioctl(d, DIOCRSETADDRS, io) 779 | 780 | return (io.pfrio_ndel, io.pfrio_nadd, io.pfrio_nchange) 781 | 782 | def get_addrs(self, table, buf_size=10): 783 | """Get the addresses in the specified table. 784 | 785 | 'table' can be either a PFTable instance or a string containing the 786 | table name. Return a list of PFTableAddr objects. 787 | """ 788 | if isinstance(table, str): 789 | table = pfr_table(pfrt_name=table.encode()) 790 | else: 791 | table = pfr_table(pfrt_name=table.name.encode(), 792 | pfrt_anchor=table.anchor.encode()) 793 | 794 | io = pfioc_table(pfrio_table=table, pfrio_esize=sizeof(pfr_addr)) 795 | 796 | with open(self.dev, 'w') as d: 797 | while True: 798 | buffer = (pfr_addr * buf_size)() 799 | io.pfrio_buffer = addressof(buffer) 800 | io.pfrio_size = buf_size 801 | 802 | ioctl(d, DIOCRGETADDRS, io) 803 | 804 | if io.pfrio_size <= buf_size: 805 | break 806 | buf_size = io.pfrio_size 807 | 808 | return tuple([PFTableAddr(a) for a in buffer[:io.pfrio_size]]) 809 | 810 | def get_tstats(self, filter=None, buf_size=10): 811 | """Get statistics information for one or more tables. 812 | 813 | 'filter' is a PFTable object that allows you to specify the anchor of 814 | the tables to retrieve statistics for. Return a tuple of PFTStats 815 | objects. 816 | """ 817 | io = pfioc_table(pfrio_esize=sizeof(pfr_tstats)) 818 | 819 | if filter is not None: 820 | io.pfrio_table = pfr_table(pfrt_anchor=filter.anchor) 821 | 822 | with open(self.dev, 'w') as d: 823 | while True: 824 | buffer = (pfr_tstats * buf_size)() 825 | io.pfrio_buffer = addressof(buffer) 826 | io.pfrio_size = buf_size 827 | 828 | ioctl(d, DIOCRGETTSTATS, io) 829 | 830 | if io.pfrio_size <= buf_size: 831 | break 832 | buf_size = io.pfrio_size 833 | 834 | stats = [] 835 | for t in buffer[:io.pfrio_size]: 836 | if t.pfrts_tzero: 837 | stats.append(PFTStats(t)) 838 | 839 | return tuple(stats) 840 | 841 | def clear_tstats(self, *tables): 842 | """Clear the statistics of one or more tables. 843 | 844 | 'tables' must be PFTable objects. Return the number of tables cleared. 845 | """ 846 | io = pfioc_table(pfrio_esize=sizeof(pfr_table), pfrio_size=len(tables)) 847 | 848 | buffer = (pfr_table * len(tables))() 849 | for (t, b) in zip(tables, buffer): 850 | b.pfrt_name = t.name.encode() 851 | b.pfrt_anchor = t.anchor.encode() 852 | 853 | io.pfrio_buffer = addressof(buffer) 854 | 855 | with open(self.dev, 'w') as d: 856 | ioctl(d, DIOCRCLRTSTATS, io) 857 | 858 | return io.pfrio_nadd 859 | 860 | def get_synflood_watermarks(self): 861 | """Return the start and end values for adaptive syncookies watermarks""" 862 | ps = pfioc_synflwats() 863 | with open(self.dev, 'w') as d: 864 | ioctl(d, DIOCGETSYNFLWATS, ps) 865 | 866 | return (ps.hiwat, ps.lowat) 867 | 868 | def set_synflood_watermarks(self, start=2500, end=1500): 869 | """Set the start and end values for adaptive syncookies watermarks""" 870 | ps = pfioc_synflwats(hiwat=start, lowat=end) 871 | with open(self.dev, 'w') as d: 872 | ioctl(d, DIOCSETSYNFLWATS, ps) 873 | 874 | def set_syncookies(self, mode): 875 | """Set the syncookies mode (never, always or adaptive)""" 876 | if mode in pf_syncookies_modes: 877 | mode = pf_syncookies_modes[mode] 878 | 879 | with open(self.dev, 'w') as d: 880 | ioctl(d, DIOCSETSYNCOOKIES, c_uint8(mode)) 881 | -------------------------------------------------------------------------------- /pf/lib.py: -------------------------------------------------------------------------------- 1 | """High-level classes to load rules more easily.""" 2 | 3 | import socket 4 | 5 | from pf.rule import PFRule, PFPool, PFPort 6 | from pf._utils import icmp_codes, icmp6_codes, icmp_types, icmp6_types 7 | from pf.constants import * 8 | 9 | 10 | __all__ = ["Rule", 11 | "BlockRule", 12 | "BlockInRule", 13 | "BlockOutRule", 14 | "PassRule", 15 | "PassInRule", 16 | "PassOutRule", 17 | "MatchRule", 18 | "MatchInRule", 19 | "MatchOutRule", 20 | "NATPool", 21 | "RDRPool", 22 | "TCPPort", 23 | "UDPPort"] 24 | 25 | 26 | # Rules 27 | class Rule(PFRule): 28 | """Generic Rule""" 29 | 30 | af = socket.AF_INET 31 | 32 | def __init__(self, **kw): 33 | kw.setdefault("af", self.af) 34 | 35 | # Try to guess protocol 36 | if "proto" not in kw: 37 | kw.setdefault("proto", 0) 38 | if not kw["proto"] and "src" in kw: 39 | kw["proto"] = kw["src"].port.proto or 0 40 | if not kw["proto"] and "dst" in kw: 41 | kw["proto"] = kw["dst"].port.proto or 0 42 | # Set default flags on TCP 'pass' rules 43 | if kw["proto"] == socket.IPPROTO_TCP and kw["action"] == PF_PASS and \ 44 | not "flags" in kw: 45 | kw.update({"flags": "S", "flagset": "SA"}) 46 | 47 | # Convert ICMP type string to constant 48 | if "type" in kw and isinstance(kw["type"], str): 49 | types = icmp6_types if (kw["af"] == socket.AF_INET6) else icmp_types 50 | for key, value in types.items(): 51 | if value == kw["type"]: 52 | kw["type"] = key + 1 53 | break 54 | else: 55 | raise ValueError("Invalid ICMP type: {.type}".format(kw)) 56 | 57 | # Convert ICMP code string to constants 58 | if "code" in kw and isinstance(kw["code"], str): 59 | codes = icmp6_codes if (kw["af"] == socket.AF_INET6) else icmp_codes 60 | for key, value in codes.items(): 61 | if value == kw["code"]: 62 | kw["type"], kw["code"] = key[0]+1, key[1]+1 63 | break 64 | else: 65 | raise ValueError("Invalid ICMP code: {.code}".format(kw)) 66 | 67 | super(Rule, self).__init__(**kw) 68 | 69 | 70 | class BlockRule(Rule): 71 | """Block (drop) all traffic""" 72 | 73 | def __init__(self, **kw): 74 | super(BlockRule, self).__init__(action=PF_DROP, **kw) 75 | 76 | 77 | class BlockInRule(BlockRule): 78 | """Block incoming traffic""" 79 | 80 | def __init__(self, **kw): 81 | super(BlockInRule, self).__init__(direction=PF_IN, **kw) 82 | 83 | 84 | class BlockOutRule(BlockRule): 85 | """Block outgoing traffic""" 86 | 87 | def __init__(self, **kw): 88 | super(BlockOutRule, self).__init__(direction=PF_OUT, **kw) 89 | 90 | 91 | class PassRule(Rule): 92 | """Pass traffic""" 93 | 94 | def __init__(self, **kw): 95 | kw.setdefault("keep_state", PF_STATE_NORMAL) 96 | super(PassRule, self).__init__(action=PF_PASS, **kw) 97 | 98 | 99 | class PassInRule(PassRule): 100 | """Pass incoming traffic""" 101 | 102 | def __init__(self, **kw): 103 | super(PassInRule, self).__init__(direction=PF_IN, **kw) 104 | 105 | 106 | class PassOutRule(PassRule): 107 | """Pass outgoing traffic""" 108 | 109 | def __init__(self, **kw): 110 | super(PassOutRule, self).__init__(direction=PF_OUT, **kw) 111 | 112 | 113 | class MatchRule(Rule): 114 | """Match traffic""" 115 | 116 | def __init__(self, **kw): 117 | super(MatchRule, self).__init__(action=PF_MATCH, **kw) 118 | 119 | 120 | class MatchInRule(MatchRule): 121 | """Match incoming traffic""" 122 | 123 | def __init__(self, **kw): 124 | super(MatchInRule, self).__init__(direction=PF_IN, **kw) 125 | 126 | 127 | class MatchOutRule(MatchRule): 128 | """Match outgoing traffic""" 129 | 130 | def __init__(self, **kw): 131 | super(MatchOutRule, self).__init__(direction=PF_OUT, **kw) 132 | 133 | 134 | # Pools 135 | class NATPool(PFPool): 136 | """NAT address pool""" 137 | 138 | def __init__(self, pool, **kw): 139 | super(NATPool, self).__init__(PF_POOL_NAT, pool, **kw) 140 | 141 | 142 | class RDRPool(PFPool): 143 | """Redirect adress pool""" 144 | 145 | def __init__(self, pool, **kw): 146 | super(RDRPool, self).__init__(PF_POOL_RDR, pool, **kw) 147 | 148 | 149 | # Ports 150 | class TCPPort(PFPort): 151 | """TCP network port""" 152 | 153 | def __init__(self, num, op=PF_OP_EQ): 154 | super(TCPPort, self).__init__(num, socket.IPPROTO_TCP, op) 155 | 156 | 157 | class UDPPort(PFPort): 158 | """UDP network port""" 159 | 160 | def __init__(self, num, op=PF_OP_EQ): 161 | super(UDPPort, self).__init__(num, socket.IPPROTO_UDP, op) 162 | -------------------------------------------------------------------------------- /pf/queue.py: -------------------------------------------------------------------------------- 1 | """Classes to represent Packet Filter's queueing schedulers and statistics.""" 2 | 3 | import pf._struct 4 | from pf._base import PFObject 5 | from pf.constants import * 6 | from pf._utils import rate2str 7 | 8 | 9 | __all__ = ["ServiceCurve", 10 | "FlowQueue", 11 | "PFQueue", 12 | "PFQueueStats"] 13 | 14 | 15 | class ServiceCurve(PFObject): 16 | """ """ 17 | 18 | _struct_type = pf._struct.pf_queue_scspec 19 | 20 | def __init__(self, bandwidth, burst=0, time=0): 21 | """ """ 22 | if isinstance(bandwidth, pf._struct.pf_queue_scspec): 23 | self._from_struct(bandwidth) 24 | else: 25 | self.bandwidth = bandwidth 26 | self.burst = burst 27 | self.time = time 28 | 29 | def _from_struct(self, sc): 30 | """ """ 31 | self.bandwidth = self._get_bandwidth(sc.m2) 32 | self.burst = self._get_bandwidth(sc.m1) 33 | self.time = sc.d 34 | 35 | def _to_struct(self): 36 | """ """ 37 | sc = pf._struct.pf_queue_scspec() 38 | if (isinstance(self.bandwidth, str) and 39 | self.bandwidth.endswith("%")): 40 | sc.m2.percent = int(self.bandwidth[:-1]) 41 | else: 42 | sc.m2.absolute = self.bandwidth 43 | if (isinstance(self.burst, str) and 44 | self.burst.endswith("%")): 45 | sc.m1.percent = int(self.burst[:-1]) 46 | else: 47 | sc.m1.absolute = self.burst 48 | sc.d = self.time 49 | return sc 50 | 51 | def _get_bandwidth(self, bw): 52 | """ """ 53 | return "{}%".format(bw.percent) if bw.percent else bw.absolute 54 | 55 | def _str_bandwidth(self, bw): 56 | """ """ 57 | return bw if isinstance(bw, str) else rate2str(bw) 58 | 59 | def _to_string(self): 60 | """ """ 61 | s = self._str_bandwidth(self.bandwidth) 62 | if self.time: 63 | s += " burst {}".format(self._str_bandwidth(self.burst)) 64 | s += " for {.time}ms".format(self) 65 | 66 | return s 67 | 68 | 69 | class FlowQueue(PFObject): 70 | """ """ 71 | 72 | _struct_type = pf._struct.pf_queue_fqspec 73 | 74 | def __init__(self, flows, quantum=0, target=0, interval=0): 75 | """ """ 76 | if isinstance(flows, pf._struct.pf_queue_fqspec): 77 | self._from_struct(flows) 78 | else: 79 | self.flows = flows 80 | self.quantum = quantum 81 | self.target = target * 1000000 82 | self.interval = interval * 1000000 83 | 84 | def _from_struct(self, fq): 85 | """ """ 86 | self.flows = fq.flows 87 | self.quantum = fq.quantum 88 | self.target = fq.target 89 | self.interval = fq.interval 90 | 91 | def _to_struct(self): 92 | """ """ 93 | fq = pf._struct.pf_queue_fqspec() 94 | fq.flows = self.flows 95 | fq.quantum = self.quantum 96 | fq.target = self.target 97 | fq.interval = self.interval 98 | return fq 99 | 100 | def _to_string(self): 101 | """ """ 102 | s = "flows {.flows}".format(self) 103 | if self.quantum: 104 | s += " quantum {.quantum}".format(self) 105 | if self.interval: 106 | s += " interval {}ms".format(self.interval / 1000000) 107 | if self.target: 108 | s += " target {}ms".format(self.target / 1000000) 109 | return s 110 | 111 | 112 | class PFQueue(PFObject): 113 | """ """ 114 | 115 | _struct_type = pf._struct.pf_queuespec 116 | 117 | def __init__(self, queue=None, **kw): 118 | """ """ 119 | if isinstance(queue, str): 120 | queue = pf._struct.pf_queuespec(qname=queue, qlimit=DEFAULT_QLIMIT) 121 | elif queue is None: 122 | queue = pf._struct.pf_queuespec() 123 | super(PFQueue, self).__init__(queue, **kw) 124 | self.stats = PFQueueStats() 125 | 126 | def _from_struct(self, q): 127 | """ """ 128 | self.qname = q.qname.decode() 129 | self.parent = q.parent.decode() 130 | self.ifname = q.ifname.decode() 131 | self.flags = q.flags 132 | self.qlimit = q.qlimit 133 | self.qid = q.qid 134 | self.parent_qid = q.parent_qid 135 | self.realtime = ServiceCurve(q.realtime) 136 | self.linkshare = ServiceCurve(q.linkshare) 137 | self.upperlimit = ServiceCurve(q.upperlimit) 138 | self.flowqueue = FlowQueue(q.flowqueue) 139 | 140 | def _to_struct(self): 141 | """ """ 142 | q = pf._struct.pf_queuespec() 143 | q.qname = self.qname.encode() 144 | q.parent = self.parent.encode() 145 | q.ifname = self.ifname.encode() 146 | q.flags = self.flags 147 | q.qlimit = self.qlimit 148 | q.qid = self.qid 149 | q.parent_qid = self.parent_qid 150 | q.realtime = self.realtime._to_struct() 151 | q.linkshare = self.linkshare._to_struct() 152 | q.upperlimit = self.upperlimit._to_struct() 153 | q.flowqueue = self.flowqueue._to_struct() 154 | return q 155 | 156 | def _to_string(self): 157 | """ """ 158 | s = "queue {.qname}".format(self) 159 | if self.parent and not self.parent.startswith("_"): 160 | s += " parent {.parent}".format(self) 161 | elif self.ifname: 162 | s += " on {.ifname}".format(self) 163 | if self.flags & PFQS_FLOWQUEUE: 164 | s += " {.flowqueue}".format(self) 165 | if self.linkshare.bandwidth or self.linkshare.burst: 166 | s += " bandwidth {}".format(self.linkshare) 167 | if self.realtime.bandwidth: 168 | s += ", min {}".format(self.realtime) 169 | if self.upperlimit.bandwidth: 170 | s += ", max {}".format(self.upperlimit) 171 | if self.flags & PFQS_DEFAULT: 172 | s += " default" 173 | if self.qlimit: 174 | s += " qlimit {.qlimit}".format(self) 175 | 176 | return s 177 | 178 | 179 | class PFQueueStats(PFObject): 180 | """ """ 181 | 182 | _struct_type = pf._struct.hfsc_class_stats 183 | 184 | def __init__(self, stats=None): 185 | """ """ 186 | if stats is None: 187 | stats = pf._struct.hfsc_class_stats() 188 | super(PFQueueStats, self).__init__(stats) 189 | 190 | def _from_struct(self, s): 191 | """ """ 192 | self.qlength = s.qlength 193 | self.qlimit = s.qlimit 194 | self.packets = (s.xmit_cnt.packets, s.drop_cnt.packets) 195 | self.bytes = (s.xmit_cnt.bytes, s.drop_cnt.bytes) 196 | 197 | def _to_string(self): 198 | """ """ 199 | s = " [ pkts: {0.packets[0]:10} bytes: {0.bytes[0]:10} " + \ 200 | "dropped pkts: {0.packets[1]:6} bytes: {0.bytes[1]:6} ]\n" + \ 201 | " [ qlength: {0.qlength:3}/{0.qlimit:3} ]" 202 | 203 | return s.format(self) 204 | 205 | -------------------------------------------------------------------------------- /pf/rule.py: -------------------------------------------------------------------------------- 1 | """Classes to represent Packet Filter Rules.""" 2 | 3 | from socket import * 4 | from ctypes import * 5 | import re 6 | import pwd 7 | import grp 8 | 9 | from pf.exceptions import PFError 10 | from pf.constants import * 11 | from pf._struct import * 12 | from pf._base import PFObject 13 | from pf._utils import * 14 | from pf.table import PFTable 15 | 16 | 17 | __all__ = ['PFUid', 18 | 'PFGid', 19 | 'PFPort', 20 | 'PFAddr', 21 | 'PFRuleAddr', 22 | 'PFPool', 23 | 'PFDivert', 24 | 'PFThreshold', 25 | 'PFRule', 26 | 'PFRuleset'] 27 | 28 | 29 | # Helper functions 30 | def azero(seq): 31 | """Return True if all numbers in 'seq' are 0s.""" 32 | return all(v == 0 for v in seq) 33 | 34 | 35 | class PFOp(PFObject): 36 | """Class representing a generic comparison operation.""" 37 | 38 | def __init__(self, num=None, op=PF_OP_NONE): 39 | """Check arguments and initialize instance attributes.""" 40 | self.op = op 41 | 42 | if isinstance(num, str) or isinstance(num, Structure): 43 | super(PFOp, self).__init__(num) 44 | elif num is None: 45 | self.num = (0, 0) 46 | elif isinstance(num, int): 47 | self.num = (num, 0) 48 | elif isinstance(num, tuple): 49 | self.num = num 50 | 51 | def _from_struct(self, operation): 52 | """Initalize a new instance from a structure.""" 53 | raise NotImplementedError 54 | 55 | def _from_string(self, operation): 56 | """Initalize a new instance from a string.""" 57 | op_re = "(?:(?P[0-9]+)?\s*" + \ 58 | "(?P=|!=|<>|><|<|<=|>|>=|:))?\s*" + \ 59 | "(?P[0-9a-z-]+)?" 60 | 61 | m = re.compile(op_re).match(operation) 62 | if not m: 63 | raise ValueError("Could not parse string: {}".format(operation)) 64 | 65 | self.op = pf_ops[m.group("op")] if m.group("op") else PF_OP_EQ 66 | 67 | try: 68 | n2 = int(m.group("n2")) 69 | except ValueError: 70 | if self.op in (PF_OP_EQ, PF_OP_NE): 71 | n2 = self._str_to_num(m.group("n2")) 72 | else: 73 | raise 74 | 75 | if self.op in (PF_OP_IRG, PF_OP_XRG, PF_OP_RRG): 76 | n1 = int(m.group("n1")) 77 | self.num = (n1, n2) 78 | else: 79 | self.num = (n2, 0) 80 | 81 | def _to_struct(self): 82 | """Return the structure representing the operation.""" 83 | raise NotImplementedError 84 | 85 | def _to_string(self): 86 | """Return the string representation of the operation.""" 87 | n1, n2 = self.num 88 | 89 | if self.op == PF_OP_NONE and not n1: 90 | return "" 91 | 92 | if self.op in (PF_OP_EQ, PF_OP_NE): 93 | n1 = self._num_to_str(n1) 94 | 95 | s = {PF_OP_NONE: "{0}", 96 | PF_OP_IRG: "{0} >< {1}", 97 | PF_OP_XRG: "{0} <> {1}", 98 | PF_OP_EQ: "= {0}", 99 | PF_OP_NE: "!= {0}", 100 | PF_OP_LT: "< {0}", 101 | PF_OP_LE: "<= {0}", 102 | PF_OP_GT: "> {0}", 103 | PF_OP_GE: ">= {0}", 104 | PF_OP_RRG: "{0}:{1}"}[self.op] 105 | 106 | return s.format(n1, n2) 107 | 108 | def _num_to_str(self, n): 109 | """Convert a numeric operand to a string.""" 110 | raise NotImplementedError 111 | 112 | def _str_to_num(self, s): 113 | """Convert a string to a numeric operand.""" 114 | raise NotImplementedError 115 | 116 | def __eq__(self, operation): 117 | return (self.num == operation.num and self.op == operation.op) 118 | 119 | def __ne__(self, operation): 120 | return not self.__eq__(operation) 121 | 122 | 123 | class PFUid(PFOp): 124 | """Class representing a user ID.""" 125 | 126 | _struct_type = pf_rule_uid 127 | 128 | def __init__(self, num=None, op=PF_OP_NONE): 129 | """Check arguments and initialize instance attributes.""" 130 | super(PFUid, self).__init__(num, op) 131 | 132 | def _from_struct(self, uid): 133 | """Initialize a new instance from a pf_rule_uid structure.""" 134 | self.num = tuple(uid.uid) 135 | self.op = uid.op 136 | 137 | def _to_struct(self): 138 | """Convert this instance to a pf_rule_uid structure.""" 139 | return pf_rule_uid(self.num, self.op) 140 | 141 | def _num_to_str(self, n): 142 | """Convert a numeric user ID to a string.""" 143 | try: 144 | return pwd.getpwuid(n).pw_name 145 | except KeyError: 146 | return n 147 | 148 | def _str_to_num(self, s): 149 | """Convert a string to a numeric user ID.""" 150 | return pwd.getpwnam(s).pw_uid 151 | 152 | 153 | class PFGid(PFOp): 154 | """Class representing a group ID.""" 155 | 156 | _struct_type = pf_rule_gid 157 | 158 | def __init__(self, num=None, op=PF_OP_NONE): 159 | """Check arguments and initialize instance attributes.""" 160 | super(PFGid, self).__init__(num, op) 161 | 162 | def _from_struct(self, gid): 163 | """Initialize a new instance from a pf_rule_gid structure.""" 164 | self.num = tuple(gid.gid) 165 | self.op = gid.op 166 | 167 | def _to_struct(self): 168 | """Convert this instance to a pf_rule_gid structure.""" 169 | return pf_rule_gid(self.num, self.op) 170 | 171 | def _num_to_str(self, n): 172 | """Convert a numeric group ID to a string.""" 173 | try: 174 | return grp.getgrgid(n).gr_name 175 | except KeyError: 176 | return n 177 | 178 | def _str_to_num(self, s): 179 | """Convert a string to a numeric group ID.""" 180 | return grp.getgrnam(s).gr_gid 181 | 182 | 183 | class PFPort(PFOp): 184 | """Class representing a TCP/UDP port.""" 185 | 186 | def __init__(self, num=None, proto=None, op=PF_OP_NONE): 187 | """Check arguments and initialize instance attributes.""" 188 | self.proto = proto 189 | super(PFPort, self).__init__(num, op) 190 | 191 | def _num_to_str(self, n): 192 | """Convert a numeric port to a service name.""" 193 | return n 194 | #try: 195 | # return getservbyport(n, getprotobynumber(self.proto)) 196 | #except (TypeError, error): 197 | # return n 198 | 199 | def _str_to_num(self, s): 200 | """Convert a service name to a numeric port.""" 201 | return getservbyname(s, getprotobynumber(self.proto)) 202 | 203 | def __eq__(self, p): 204 | return (self.num == p.num and 205 | self.op == p.op and 206 | self.proto == p.proto) 207 | 208 | 209 | class PFAddr(PFObject): 210 | """Class representing an address.""" 211 | 212 | _struct_type = pf_addr_wrap 213 | 214 | def __init__(self, addr=None, af=AF_UNSPEC, **kw): 215 | """Check arguments and initialize instance attributes.""" 216 | self.af = af 217 | 218 | if addr is None: 219 | t = kw.get("type", PF_ADDR_ADDRMASK) 220 | addr = pf_addr_wrap(type=t) 221 | 222 | super(PFAddr, self).__init__(addr, **kw) 223 | 224 | def _from_struct(self, a): 225 | """Initalize a new instance from a pf_addr_wrap structure.""" 226 | self.type = a.type 227 | a6, m6 = a.v.a.addr.v6, a.v.a.mask.v6 228 | 229 | if self.type == PF_ADDR_DYNIFTL and self.af != AF_UNSPEC or \ 230 | self.type == PF_ADDR_ADDRMASK and not (azero(a6) and azero(m6)) or \ 231 | self.type == PF_ADDR_RANGE: 232 | try: 233 | l = {AF_INET: 4, AF_INET6: 16}[self.af] 234 | except KeyError: 235 | raise PFError("No valid address family specified") 236 | else: 237 | addr = inet_ntop(self.af, string_at(addressof(a6), l)) 238 | mask = inet_ntop(self.af, string_at(addressof(m6), l)) 239 | else: 240 | addr = mask = None 241 | 242 | if self.type == PF_ADDR_DYNIFTL: 243 | self.ifname = a.v.ifname.decode() 244 | self.iflags = a.iflags 245 | self.dyncnt = a.p.dyncnt 246 | self.mask = mask 247 | elif self.type == PF_ADDR_TABLE: 248 | self.tblname = a.v.tblname.decode() 249 | self.tblcnt = a.p.tblcnt 250 | elif self.type == PF_ADDR_ADDRMASK: 251 | self.addr = addr 252 | self.mask = mask 253 | elif self.type == PF_ADDR_RTLABEL: 254 | self.rtlabelname = a.v.rtlabelname.decode() 255 | self.rtlabel = a.v.rtlabel.decode() 256 | elif self.type == PF_ADDR_RANGE: 257 | self.addr = (addr, mask) 258 | 259 | def _from_string(self, a): 260 | """Initalize a new instance from a string.""" 261 | ipv4_re = "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" 262 | ipv6_re = "[0-9a-f:]+" 263 | addr_re = "(?Pno-route)|" + \ 264 | "(?Purpf-failed)|" + \ 265 | "(?Pany)|" + \ 266 | "(?P<(?P\w+)>)|" + \ 267 | "(?Proute\s+(?P\w+))|" + \ 268 | "(?P\((?P[a-z]+[0-9]*)" + \ 269 | "(?P(:network|:broadcast|:peer|:0)*)\)" + \ 270 | "(?:/(?P\d+))?)|" + \ 271 | "(?P(?P{})\s*-\s*".format(ipv4_re) + \ 272 | "(?P{}))|".format(ipv4_re) + \ 273 | "(?P(?P{})\s*-\s*".format(ipv6_re) + \ 274 | "(?P{}))|".format(ipv6_re) + \ 275 | "(?P{})(?:/(?P\d+))?|".format(ipv4_re) + \ 276 | "(?P{})(?:/(?P\d+))?".format(ipv6_re) 277 | 278 | m = re.compile(addr_re).match(a) 279 | if not m: 280 | raise ValueError("Could not parse address: {}".format(a)) 281 | 282 | if m.group("nort"): 283 | self.type = PF_ADDR_NOROUTE 284 | elif m.group("urpf"): 285 | self.type = PF_ADDR_URPFFAILED 286 | elif m.group("any"): 287 | self.type = PF_ADDR_ADDRMASK 288 | self.addr = self.mask = None 289 | elif m.group("tbl"): 290 | self.type = PF_ADDR_TABLE 291 | self.tblname = m.group("tblname") 292 | self.tblcnt = -1 293 | elif m.group("rt"): 294 | self.type = PF_ADDR_RTLABEL 295 | self.rtlabelname = m.group("rtlbl") 296 | self.rtlabel = 0 297 | elif m.group("if"): 298 | self.type = PF_ADDR_DYNIFTL 299 | self.ifname = m.group("ifname") 300 | try: 301 | b = {AF_INET: 32, AF_INET6: 128}[self.af] 302 | except KeyError: 303 | self.mask = None 304 | else: 305 | if (m.group("ifmask")): 306 | self.mask = ctonm(int(m.group("ifmask")), self.af) 307 | else: 308 | self.mask = ctonm(b, self.af) 309 | self.dyncnt = 0 310 | self.iflags = 0 311 | for mod in m.group("mod").split(":")[1:]: 312 | self.iflags |= pf_if_mods[mod] 313 | elif m.group("ipv4rg"): 314 | self.af = AF_INET 315 | self.type = PF_ADDR_RANGE 316 | self.addr = m.group("ipv4_1", "ipv4_2") 317 | elif m.group("ipv6rg"): 318 | self.af = AF_INET6 319 | self.type = PF_ADDR_RANGE 320 | self.addr = m.group("ipv6_1", "ipv6_2") 321 | elif m.group("ipv4"): 322 | self.af = AF_INET 323 | self.type = PF_ADDR_ADDRMASK 324 | self.addr = m.group("ipv4") 325 | if m.group("mask4"): 326 | self.mask = ctonm(int(m.group("mask4")), self.af) 327 | else: 328 | self.mask = ctonm(32, self.af) 329 | elif m.group("ipv6"): 330 | self.af = AF_INET6 331 | self.type = PF_ADDR_ADDRMASK 332 | self.addr = m.group("ipv6") 333 | if m.group("mask6"): 334 | self.mask = ctonm(int(m.group("mask6")), self.af) 335 | else: 336 | self.mask = ctonm(128, self.af) 337 | 338 | def _to_struct(self): 339 | """Convert this instance to a pf_addr_wrap structure.""" 340 | a = pf_addr_wrap() 341 | a.type = self.type 342 | 343 | if self.type == PF_ADDR_DYNIFTL: 344 | a.v.ifname = self.ifname.encode() 345 | a.p.dyncnt = self.dyncnt 346 | a.iflags = self.iflags 347 | if self.af == AF_UNSPEC: 348 | mask = b'\xff' * 16 349 | else: 350 | mask = inet_pton(self.af, self.mask) 351 | memmove(a.v.a.mask.v6, c_char_p(mask), len(mask)) 352 | elif self.type == PF_ADDR_TABLE: 353 | a.v.tblname = self.tblname.encode() 354 | a.p.tblcnt = self.tblcnt 355 | elif self.type == PF_ADDR_ADDRMASK and self.addr: 356 | addr = inet_pton(self.af, self.addr) 357 | mask = inet_pton(self.af, self.mask) 358 | memmove(a.v.a.addr.v6, c_char_p(addr), len(addr)) 359 | memmove(a.v.a.mask.v6, c_char_p(mask), len(mask)) 360 | elif self.type == PF_ADDR_RTLABEL: 361 | a.v.rtlabelname = self.rtlabelname.encode() 362 | a.v.rtlabel = self.rtlabel 363 | elif self.type == PF_ADDR_RANGE: 364 | addr1 = inet_pton(self.af, self.addr[0]) 365 | addr2 = inet_pton(self.af, self.addr[1]) 366 | memmove(a.v.a.addr.v6, c_char_p(addr1), len(addr1)) 367 | memmove(a.v.a.mask.v6, c_char_p(addr2), len(addr2)) 368 | 369 | return a 370 | 371 | def _to_string(self): 372 | """Return the string representation of the address.""" 373 | if self.type == PF_ADDR_DYNIFTL: 374 | s = "({.ifname}".format(self) 375 | if self.iflags & PFI_AFLAG_NETWORK: 376 | s += ":network" 377 | if self.iflags & PFI_AFLAG_BROADCAST: 378 | s += ":broadcast" 379 | if self.iflags & PFI_AFLAG_PEER: 380 | s += ":peer" 381 | if self.iflags & PFI_AFLAG_NOALIAS: 382 | s += ":0" 383 | s += ")" 384 | elif self.type == PF_ADDR_TABLE: 385 | return "<{.tblname}>".format(self) 386 | elif self.type == PF_ADDR_ADDRMASK: 387 | s = self.addr or "any" 388 | elif self.type == PF_ADDR_NOROUTE: 389 | return "no-route" 390 | elif self.type == PF_ADDR_URPFFAILED: 391 | return "urpf-failed" 392 | elif self.type == PF_ADDR_RTLABEL: 393 | return "route \"{.rtlabelname}\"".format(self) 394 | elif self.type == PF_ADDR_RANGE: 395 | s = "{0.addr[0]} - {0.addr[1]}".format(self) 396 | else: 397 | return "?" 398 | 399 | if self.type != PF_ADDR_RANGE and self.mask: 400 | bits = nmtoc(self.mask, self.af) 401 | if not ((self.af == AF_INET and bits == 32) or (bits == 128)): 402 | s += "/{}".format(bits) 403 | 404 | return s 405 | 406 | def _is_any(self): 407 | """Return true if this address matches any host.""" 408 | return (self.type == PF_ADDR_ADDRMASK and self.addr is None) 409 | 410 | def __eq__(self, a): 411 | if (self.type != a.type or self.af != a.af): 412 | return False 413 | 414 | if self.type == PF_ADDR_DYNIFTL: 415 | return (self.ifname == a.ifname and 416 | self.mask == a.mask and 417 | self.iflags == a.iflags) 418 | elif self.type == PF_ADDR_TABLE: 419 | return (self.tblname == a.tblname) 420 | elif self.type == PF_ADDR_ADDRMASK: 421 | return (self.addr == a.addr and 422 | self.mask == a.mask) 423 | elif self.type == PF_ADDR_RTLABEL: 424 | return (self.rtlabelname == a.rtlabelname.decode()) 425 | elif self.type == PF_ADDR_RANGE: 426 | return (self.addr == a.addr) 427 | 428 | return True 429 | 430 | def __ne__(self, a): 431 | return not self.__eq__(a) 432 | 433 | 434 | class PFRuleAddr(PFObject): 435 | """Class representing an address/port pair.""" 436 | 437 | _struct_type = pf_rule_addr 438 | 439 | def __init__(self, addr=None, port=None, neg=False, **kw): 440 | """Check arguments and initialize instance attributes.""" 441 | if isinstance(addr, self._struct_type): 442 | self.addr = PFAddr(addr.addr, kw['af']) 443 | self.port = PFPort(tuple(map(ntohs, addr.port)), 444 | kw['proto'], addr.port_op) 445 | self.neg = bool(addr.neg) 446 | self.weight = addr.weight 447 | else: 448 | self.addr = addr or PFAddr() 449 | self.port = port or PFPort() 450 | self.neg = bool(neg) 451 | self.weight = 0 452 | 453 | def _to_struct(self): 454 | """Convert this instance to a pf_rule_addr structure.""" 455 | a = pf_rule_addr() 456 | 457 | a.addr = self.addr._to_struct() 458 | a.port = tuple(map(htons, self.port.num)) 459 | a.port_op = self.port.op 460 | a.neg = int(self.neg) 461 | a.weight = self.weight 462 | 463 | return a 464 | 465 | def _to_string(self): 466 | """Return the string representation of the address/port pair.""" 467 | s = ("! {.addr}" if self.neg else "{.addr}").format(self) 468 | p = "{.port}".format(self) 469 | if p: 470 | s += (":" if self.port.op == PF_OP_NONE else " port ") + p 471 | 472 | return s 473 | 474 | def __eq__(self, a): 475 | return (self.addr == a.addr and 476 | self.port == a.port and 477 | self.neg == a.neg) 478 | 479 | def __ne__(self, a): 480 | return not self.__eq__(a) 481 | 482 | 483 | class PFPool(PFObject): 484 | """Class representing an address pool.""" 485 | 486 | _struct_type = pf_pool 487 | 488 | def __init__(self, id, pool, **kw): 489 | """Check arguments and initialize instance attributes.""" 490 | self.id = id 491 | 492 | if isinstance(pool, PFAddr): 493 | self._af = pool.af 494 | p = pf_pool(addr=pool._to_struct()) 495 | if self.id == PF_POOL_NAT: 496 | p.proxy_port = (PF_NAT_PROXY_PORT_LOW, PF_NAT_PROXY_PORT_HIGH) 497 | elif isinstance(pool, self._struct_type): 498 | self._af = kw.pop("af", AF_UNSPEC) 499 | p = pool 500 | 501 | super(PFPool, self).__init__(p, **kw) 502 | 503 | def _from_struct(self, p): 504 | """Initalize a new instance from a pf_pool structure.""" 505 | self.addr = PFAddr(p.addr, self._af) 506 | self.key = "{:#010x}{:08x}{:08x}{:08x}".format(*p.key.key32) 507 | self.counter = p.counter 508 | self.ifname = p.ifname.decode() 509 | self.tblidx = p.tblidx 510 | self.states = p.states 511 | self.curweight = p.curweight 512 | self.weight = p.weight 513 | self.proxy_port = PFPort(tuple(p.proxy_port), op=p.port_op) 514 | self.opts = p.opts 515 | 516 | def _to_struct(self): 517 | """Convert a PFPool object to a pf_pool structure.""" 518 | p = pf_pool() 519 | 520 | if self.addr._is_any(): 521 | p.addr = PFAddr(type=PF_ADDR_NONE)._to_struct() 522 | else: 523 | p.addr = self.addr._to_struct() 524 | p.ifname = self.ifname.encode() 525 | p.states = self.states 526 | p.curweight = self.curweight 527 | p.weight = self.weight 528 | p.proxy_port = self.proxy_port.num 529 | p.port_op = self.proxy_port.op 530 | p.opts = self.opts 531 | 532 | return p 533 | 534 | def _to_string(self): 535 | """Return the string representation of the address pool.""" 536 | p1, p2 = self.proxy_port.num 537 | s = "" 538 | 539 | if self.ifname: 540 | if self.addr.addr is not None: 541 | s += "{.addr}@".format(self) 542 | s += self.ifname 543 | else: 544 | s += "{.addr}".format(self) 545 | 546 | if self.id == PF_POOL_NAT: 547 | if (p1, p2) != (PF_NAT_PROXY_PORT_LOW, PF_NAT_PROXY_PORT_HIGH) and \ 548 | (p1, p2) != (0, 0): 549 | if p1 == p2: 550 | s += " port {}".format(p1) 551 | else: 552 | s += " port {}:{}".format(p1, p2) 553 | elif self.id == PF_POOL_RDR: 554 | if p1: 555 | s += " port {}".format(p1) 556 | if p2 and (p2 != p1): 557 | s += ":{}".format(p2) 558 | 559 | opt = self.opts & PF_POOL_TYPEMASK 560 | if opt == PF_POOL_BITMASK: 561 | s += " bitmask" 562 | elif opt == PF_POOL_RANDOM: 563 | s += " random" 564 | elif opt == PF_POOL_SRCHASH: 565 | s += " source-hash {0.key}".format(self) 566 | elif opt == PF_POOL_ROUNDROBIN: 567 | s += " round-robin" 568 | elif opt == PF_POOL_LEASTSTATES: 569 | s += " least-states" 570 | 571 | if self.opts & PF_POOL_STICKYADDR: 572 | s += " sticky-address" 573 | 574 | if (self.id == PF_POOL_NAT) and (p1 == p2 == 0): 575 | s += " static-port" 576 | 577 | return s 578 | 579 | 580 | class PFDivert(PFObject): 581 | """Class representing a divert socket.""" 582 | 583 | _struct_type = divert 584 | 585 | def __init__(self, type=PF_DIVERT_NONE, addr=None, port=None, af=AF_UNSPEC): 586 | """Check arguments and initialize instance attributes.""" 587 | self.af = af 588 | if isinstance(type, self._struct_type): 589 | self._from_struct(type) 590 | else: 591 | self.type = type 592 | self.addr = addr 593 | self.port = port 594 | if addr and (af == AF_UNSPEC): 595 | if is_IPaddr(addr): 596 | self.af = AF_INET 597 | elif is_IP6addr(addr): 598 | self.af = AF_INET6 599 | 600 | def _from_struct(self, d): 601 | """ Initalize a new instance from a divert structure.""" 602 | self.type = d.type 603 | self.addr = None 604 | self.port = None 605 | 606 | if self.type == PF_DIVERT_TO: 607 | self.addr = inet_ntop(self.af, string_at(addressof(d.addr), l)) 608 | self.port = ntohs(d.port) 609 | elif self.type == PF_DIVERT_PACKET: 610 | self.port = ntohs(d.port) 611 | 612 | def _to_struct(self): 613 | """Convert a PFDivert object to a divert structure.""" 614 | d = divert(type=self.type) 615 | if self.type in (PF_DIVERT_TO, PF_DIVERT_PACKET): 616 | d.port = htons(self.port) 617 | if self.type == PF_DIVERT_TO: 618 | d.addr = inet_pton(self.af, self.addr) 619 | 620 | return d 621 | 622 | def _to_string(self): 623 | """Return a string representation of the object.""" 624 | if self.type == PF_DIVERT_NONE: 625 | return "" 626 | if self.type == PF_DIVERT_TO: 627 | return "divert-to {.addr} port {.port}".format(self) 628 | elif self.type == PF_DIVERT_REPLY: 629 | return "divert-reply" 630 | elif self.type == PF_DIVERT_PACKET: 631 | return "divert-packet port {.port}".format(self) 632 | else: 633 | return "divert ???" 634 | 635 | 636 | class PFThreshold(PFObject): 637 | """Measure the rate of packets matching the rule and states created by it""" 638 | 639 | _struct_type = pf_threshold 640 | 641 | def __init__(self, limit, seconds=0): 642 | """Check arguments and initialize instance attributes""" 643 | if not isinstance(limit, self._struct_type): 644 | limit = pf_threshold(limit=limit, seconds=seconds) 645 | self._from_struct(limit) 646 | 647 | def _from_struct(self, threshold): 648 | """Initalize a new instance from a pf_threshold structure""" 649 | self.limit = threshold.limit 650 | self.seconds = threshold.seconds 651 | self.count = threshold.count 652 | self.last = threshold.last 653 | 654 | def _to_struct(self): 655 | """Convert a PFThreshold object to a pf_threshold structure.""" 656 | threshold = pf_threshold() 657 | 658 | threshold.limit = self.limit 659 | threshold.seconds = self.seconds 660 | threshold.count = self.count 661 | threshold.last = self.last 662 | 663 | return threshold 664 | 665 | def _to_string(self): 666 | """Return a string representation of the object.""" 667 | return "max-pkt-rate {.limit}/{.seconds}".format(self) 668 | 669 | 670 | class PFRule(PFObject): 671 | """Class representing a Packet Filter rule.""" 672 | 673 | _struct_type = pf_rule 674 | 675 | def __init__(self, rule=None, **kw): 676 | """Check arguments and initialize instance attributes.""" 677 | if rule is None: 678 | rule = pf_rule(rtableid=-1, onrdomain=-1) 679 | super(PFRule, self).__init__(rule, **kw) 680 | 681 | def _from_struct(self, r): 682 | """Initalize a new instance from a pf_rule structure.""" 683 | self.src = PFRuleAddr(r.src, af=r.af, proto=r.proto) 684 | self.dst = PFRuleAddr(r.dst, af=r.af, proto=r.proto) 685 | #skip 686 | self.label = r.label 687 | self.ifname = r.ifname.decode() 688 | self.rcv_ifname = r.rcv_ifname.decode() 689 | self.qname = r.qname 690 | self.pqname = r.pqname 691 | self.tagname = r.tagname 692 | self.match_tagname = r.match_tagname 693 | self.overload_tblname = r.overload_tblname 694 | #entries 695 | 696 | self.nat = PFPool(PF_POOL_NAT, r.nat, af=r.af) 697 | if self.nat.addr._is_any(): 698 | self.nat = PFPool(PF_POOL_NAT, PFAddr(type=PF_ADDR_NONE)) 699 | self.rdr = PFPool(PF_POOL_RDR, r.rdr, af=r.af) 700 | if self.rdr.addr._is_any(): 701 | self.rdr = PFPool(PF_POOL_RDR, PFAddr(type=PF_ADDR_NONE)) 702 | self.route = PFPool(PF_POOL_ROUTE, r.route, af=r.af) 703 | if self.route.addr._is_any(): 704 | self.route = PFPool(PF_POOL_ROUTE, PFAddr(type=PF_ADDR_NONE)) 705 | 706 | self.pktrate = PFThreshold(r.pktrate) 707 | self.evaluations = r.evaluations 708 | self.packets = tuple(r.packets) 709 | self.bytes = tuple(r.bytes) 710 | self.os_fingerprint = r.os_fingerprint 711 | self.rtableid = r.rtableid 712 | self.onrdomain = r.onrdomain 713 | self.timeout = list(r.timeout) 714 | self.states_cur = r.states_cur 715 | self.states_tot = r.states_tot 716 | self.max_states = r.max_states 717 | self.src_nodes = r.src_nodes 718 | self.max_src_nodes = r.max_src_nodes 719 | self.max_src_states = r.max_src_states 720 | self.max_src_conn = r.max_src_conn 721 | self.max_src_conn_rate = (r.max_src_conn_rate.limit, 722 | r.max_src_conn_rate.seconds) 723 | self.qid = r.qid 724 | self.pqid = r.pqid 725 | self.rt_listid = r.rt_listid 726 | self.nr = r.nr 727 | self.prob = r.prob 728 | self.cuid = r.cuid 729 | self.cpid = r.cpid 730 | self.return_icmp = r.return_icmp 731 | self.return_icmp6 = r.return_icmp6 732 | self.max_mss = r.max_mss 733 | self.tag = r.tag 734 | self.match_tag = r.match_tag 735 | self.scrub_flags = r.scrub_flags 736 | self.delay = r.delay 737 | self.uid = PFUid(r.uid) 738 | self.gid = PFGid(r.gid) 739 | self.rule_flag = r.rule_flag 740 | self.action = r.action 741 | self.direction = r.direction 742 | self.log = r.log 743 | self.logif = r.logif 744 | self.quick = bool(r.quick) 745 | self.ifnot = bool(r.ifnot) 746 | self.match_tag_not = bool(r.match_tag_not) 747 | self.keep_state = r.keep_state 748 | self.af = r.af 749 | self.proto = r.proto 750 | self.type = r.type 751 | self.code = r.code 752 | self.flags = "".join([f for n, f in enumerate("FSRPAUEW") 753 | if r.flags & (1 << n)]) 754 | self.flagset = "".join([f for n, f in enumerate("FSRPAUEW") 755 | if r.flagset & (1 << n)]) 756 | self.min_ttl = r.min_ttl 757 | self.allow_opts = bool(r.allow_opts) 758 | self.rt = r.rt 759 | self.return_ttl = r.return_ttl 760 | self.tos = r.tos 761 | self.set_tos = r.set_tos 762 | self.anchor_relative = r.anchor_relative 763 | self.anchor_wildcard = r.anchor_wildcard 764 | self.flush = r.flush 765 | self.prio = r.prio 766 | self.set_prio = tuple(r.set_prio) 767 | self.naf = r.naf 768 | self.divert = PFDivert(r.divert, af=self.af) 769 | 770 | def _to_struct(self): 771 | """Convert a PFRule object to a pf_rule structure.""" 772 | r = pf_rule() 773 | 774 | r.src = self.src._to_struct() 775 | r.dst = self.dst._to_struct() 776 | #skip 777 | r.label = self.label 778 | r.ifname = self.ifname.encode() 779 | r.rcv_ifname = self.rcv_ifname.encode() 780 | r.qname = self.qname 781 | r.pqname = self.pqname 782 | r.tagname = self.tagname 783 | r.match_tagname = self.match_tagname 784 | r.overload_tblname = self.overload_tblname 785 | #entries 786 | r.nat = self.nat._to_struct() 787 | r.rdr = self.rdr._to_struct() 788 | r.route = self.route._to_struct() 789 | r.pktrate = self.pktrate._to_struct() 790 | r.evaluations = self.evaluations 791 | r.packets = self.packets 792 | r.bytes = self.bytes 793 | r.os_fingerprint = self.os_fingerprint 794 | r.rtableid = self.rtableid 795 | r.onrdomain = self.onrdomain 796 | for i, t in enumerate(self.timeout): 797 | r.timeout[i] = t 798 | r.states_cur = self.states_cur 799 | r.states_tot = self.states_tot 800 | r.max_states = self.max_states 801 | r.src_nodes = self.src_nodes 802 | r.max_src_nodes = self.max_src_nodes 803 | r.max_src_states = self.max_src_states 804 | r.max_src_conn = self.max_src_conn 805 | r.max_src_conn_rate = self.max_src_conn_rate 806 | r.qid = self.qid 807 | r.pqid = self.pqid 808 | r.rt_listid = self.rt_listid 809 | r.nr = self.nr 810 | r.prob = self.prob 811 | r.cuid = self.cuid 812 | r.cpid = self.cpid 813 | r.return_icmp = self.return_icmp 814 | r.return_icmp6 = self.return_icmp6 815 | r.max_mss = self.max_mss 816 | r.tag = self.tag 817 | r.match_tag = self.match_tag 818 | r.scrub_flags = self.scrub_flags 819 | r.delay = self.delay 820 | r.uid = self.uid._to_struct() 821 | r.gid = self.gid._to_struct() 822 | r.rule_flag = self.rule_flag 823 | r.action = self.action 824 | r.direction = self.direction 825 | r.log = self.log 826 | r.logif = self.logif 827 | r.quick = int(self.quick) 828 | r.ifnot = int(self.ifnot) 829 | r.match_tag_not = int(self.match_tag_not) 830 | r.keep_state = self.keep_state 831 | r.af = self.af 832 | r.proto = self.proto 833 | r.type = self.type 834 | r.code = self.code 835 | r.flags = sum([1<<"FSRPAUEW".find(f) for f in self.flags]) 836 | r.flagset = sum([1<<"FSRPAUEW".find(f) for f in self.flagset]) 837 | r.min_ttl = self.min_ttl 838 | r.allow_opts = int(self.allow_opts) 839 | r.rt = self.rt 840 | r.return_ttl = self.return_ttl 841 | r.tos = self.tos 842 | r.set_tos = self.set_tos 843 | r.anchor_relative = self.anchor_relative 844 | r.anchor_wildcard = self.anchor_wildcard 845 | r.flush = self.flush 846 | r.prio = self.prio 847 | r.set_prio[:] = self.set_prio 848 | r.naf = self.naf 849 | r.divert = self.divert._to_struct() 850 | 851 | return r 852 | 853 | def _to_string(self): 854 | """Return the string representation of the rule.""" 855 | pf_actions = ("pass", "block", "scrub", "no scrub", "nat", "no nat", 856 | "binat", "no binat", "rdr", "no rdr", "", "", "match") 857 | pf_anchors = ("anchor", "anchor", "anchor", "anchor", "nat-anchor", 858 | "nat-anchor", "binat-anchor", "binat-anchor", 859 | "rdr-anchor", "rdr-anchor") 860 | 861 | if self.rule_flag & PFRULE_EXPIRED: 862 | return "" 863 | 864 | if self.action > PF_MATCH: 865 | s = "action({.action})".format(self) 866 | elif isinstance(self, PFRuleset): 867 | s = pf_anchors[self.action] 868 | if not self.name.startswith("_"): 869 | s += " \"{.name}\"".format(self) 870 | else: 871 | s = pf_actions[self.action] 872 | 873 | if self.action == PF_DROP: 874 | if self.rule_flag & PFRULE_RETURN: 875 | s += " return" 876 | elif self.rule_flag & PFRULE_RETURNRST: 877 | s += " return-rst" 878 | if self.return_ttl: 879 | s += "(ttl {.return_ttl})".format(self) 880 | elif self.rule_flag & PFRULE_RETURNICMP: 881 | ic = geticmpcodebynumber(self.return_icmp >> 8, 882 | self.return_icmp & 0xff, AF_INET) 883 | ic6 = geticmpcodebynumber(self.return_icmp6 >> 8, 884 | self.return_icmp6 & 0xff, AF_INET6) 885 | s += " return-icmp" 886 | if self.af == AF_INET: 887 | s += "({})".format(ic or self.return_icmp & 0xff) 888 | elif self.af == AF_INET6: 889 | s += "6({})".format(ic6 or self.return_icmp6 & 0xff) 890 | else: 891 | s += "({}, {})".format((ic or self.return_icmp & 0xff), 892 | (ic6 or self.return_icmp6 & 0xff)) 893 | else: 894 | s += " drop" 895 | 896 | if self.direction == PF_IN: 897 | s += " in" 898 | elif self.direction == PF_OUT: 899 | s += " out" 900 | 901 | if self.log: 902 | s += " log" 903 | if (self.log & ~PF_LOG) or self.logif: 904 | l = [] 905 | if self.log & PF_LOG_ALL: 906 | l.append("all") 907 | if self.log & PF_LOG_MATCHES: 908 | l.append("matches") 909 | if self.log & PF_LOG_SOCKET_LOOKUP: 910 | l.append("user") 911 | if self.logif: 912 | l.append("to pflog{.logif}".format(self)) 913 | s += " ({})".format(", ".join(l)) 914 | 915 | if self.quick: 916 | s += " quick" 917 | 918 | if self.ifname and self.ifname != "all": 919 | # "on all" not printed because it would make 920 | # the rule not parseable by pfctl 921 | if self.ifnot: 922 | s += " on ! {.ifname}".format(self) 923 | else: 924 | s += " on {.ifname}".format(self) 925 | 926 | if self.onrdomain >= 0: 927 | if self.ifnot: 928 | s += " on ! rdomain {.onrdomain}".format(self) 929 | else: 930 | s += " on rdomain {.onrdomain}".format(self) 931 | 932 | if self.af: 933 | s += " inet" if (self.af == AF_INET) else " inet6" 934 | 935 | if self.proto: 936 | s += " proto {}".format(getprotobynumber(self.proto) or self.proto) 937 | 938 | if self.src.addr._is_any() and self.dst.addr._is_any() and \ 939 | not self.src.neg and not self.dst.neg and \ 940 | not self.src.port.op and not self.dst.port.op and \ 941 | self.os_fingerprint == PF_OSFP_ANY: 942 | s += " all" 943 | else: 944 | s += " from {.src}".format(self) 945 | #if self.os_fingerprint != PF_OSFP_ANY: 946 | s += " to {.dst}".format(self) 947 | 948 | if self.rcv_ifname: 949 | s += " received on {.rcv_ifname}".format(self) 950 | if self.uid.op: 951 | s += " user {.uid}".format(self) 952 | if self.gid.op: 953 | s += " group {.gid}".format(self) 954 | 955 | if self.flags or self.flagset: 956 | s += " flags {0.flags}/{0.flagset}".format(self) 957 | elif self.action in (PF_PASS, PF_MATCH) and \ 958 | self.proto in (0, IPPROTO_TCP) and \ 959 | not (self.rule_flag & PFRULE_FRAGMENT) and \ 960 | not isinstance(self, PFRuleset) and \ 961 | self.keep_state: 962 | s += " flags any" 963 | 964 | if self.type: 965 | it = geticmptypebynumber(self.type-1, self.af) 966 | if self.af != AF_INET6: 967 | s += " icmp-type" 968 | else: 969 | s += " icmp6-type" 970 | s += " {}".format(it or self.type-1) 971 | if self.code: 972 | ic = geticmpcodebynumber(self.type-1, self.code-1, self.af) 973 | s += " code {}".format(ic or self.code-1) 974 | 975 | if self.tos: 976 | s += " tos {.tos:#04x}".format(self) 977 | if self.prio: 978 | prio = 0 if (self.prio == PF_PRIO_ZERO) else self.prio 979 | s += " prio {}".format(prio) 980 | if self.pktrate.limit: 981 | s += " {.pktrate}".format(self) 982 | 983 | if self.scrub_flags & PFSTATE_SETMASK or self.qname or \ 984 | self.rule_flag & PFRULE_SETDELAY: 985 | opts = [] 986 | if self.scrub_flags[0] != PFSTATE_SETPRIO: 987 | if self.set_prio[0] == self.set_prio[1]: 988 | opts.append("prio {}".format(self.set_prio[0])) 989 | else: 990 | opts.append("prio ({}, {})".format(*self.set_prio)) 991 | if self.qname: 992 | if self.pqname: 993 | opts.append("queue ({.qname}, {.pqname})".format(self)) 994 | else: 995 | opts.append("queue {.qname}".format(self)) 996 | if self.scrub_flags & PFSTATE_SETTOS: 997 | opts.append("tos {.set_tos:#04x}".format(self)) 998 | if self.rule_flag & PFRULE_SETDELAY: 999 | opts.append("delay {.delay}".format(self)) 1000 | s += " set ( {} )".format(", ".join(opts)) 1001 | 1002 | has_opts = False 1003 | if (self.max_states or self.max_src_nodes or self.max_src_states) or \ 1004 | self.rule_flag & (PFRULE_NOSYNC|PFRULE_SRCTRACK|PFRULE_IFBOUND) or \ 1005 | self.rule_flag & (PFRULE_STATESLOPPY|PFRULE_PFLOW) or \ 1006 | any(self.timeout): 1007 | has_opts = True 1008 | 1009 | if not self.keep_state and self.action == PF_PASS and \ 1010 | not isinstance(self, PFRuleset): 1011 | s += " no state" 1012 | elif (self.keep_state == PF_STATE_NORMAL) and has_opts: 1013 | s += " keep state" 1014 | elif self.keep_state == PF_STATE_MODULATE: 1015 | s += " modulate state" 1016 | elif self.keep_state == PF_STATE_SYNPROXY: 1017 | s += " synproxy state" 1018 | 1019 | if self.prob: 1020 | s += " probability {:.0f}%".format(self.prob*100.0/(UINT_MAX+1)) 1021 | 1022 | if has_opts: 1023 | opts = [] 1024 | if self.max_states: 1025 | opts.append("max {.max_states}".format(self)) 1026 | if self.rule_flag & PFRULE_NOSYNC: 1027 | opts.append("no-sync") 1028 | if self.rule_flag & PFRULE_SRCTRACK: 1029 | if self.rule_flag & PFRULE_RULESRCTRACK: 1030 | opts.append("source-track rule") 1031 | else: 1032 | opts.append("source-track global") 1033 | if self.max_src_states: 1034 | opts.append("max-src-states {.max_src_states}".format(self)) 1035 | if self.max_src_conn: 1036 | opts.append("max-src-conn {.max_src_conn}".format(self)) 1037 | if self.max_src_conn_rate[0]: 1038 | opts.append("max-src-conn-rate " + 1039 | "{}/{}".format(*self.max_src_conn_rate)) 1040 | if self.max_src_nodes: 1041 | opts.append("max-src-nodes {.max_src_nodes}".format(self)) 1042 | if self.overload_tblname: 1043 | opt = "overload <{.overload_tblname}>".format(self) 1044 | if self.flush: 1045 | opt += " flush" 1046 | if self.flush & PF_FLUSH_GLOBAL: 1047 | opt += " global" 1048 | opts.append(opt) 1049 | if self.rule_flag & PFRULE_IFBOUND: 1050 | opts.append("if-bound") 1051 | if self.rule_flag & PFRULE_STATESLOPPY: 1052 | opts.append("sloppy") 1053 | if self.rule_flag & PFRULE_PFLOW: 1054 | opts.append("pflow") 1055 | for i, t in enumerate(self.timeout): 1056 | if t: 1057 | tm = [k for (k, v) in pf_timeouts.items() if v == i][0] 1058 | opts.append("{} {}".format(tm, t)) 1059 | 1060 | s += " ({})".format(", ".join(opts)) 1061 | 1062 | if self.rule_flag & PFRULE_FRAGMENT: 1063 | s += " fragment" 1064 | 1065 | if self.scrub_flags >= PFSTATE_NODF or self.min_ttl or self.max_mss: 1066 | opts = [] 1067 | if self.scrub_flags & PFSTATE_NODF: 1068 | opts.append("no-df") 1069 | if self.scrub_flags & PFSTATE_RANDOMID: 1070 | opts.append("random-id") 1071 | if self.min_ttl: 1072 | opts.append("min-ttl {.min_ttl}".format(self)) 1073 | if self.scrub_flags & PFSTATE_SCRUB_TCP: 1074 | opts.append("reassemble tcp") 1075 | if self.max_mss: 1076 | opts.append("max_mss {.max_mss}".format(self)) 1077 | s += " scrub ({})".format(" ".join(opts)) 1078 | 1079 | if self.allow_opts: 1080 | s += " allow-opts" 1081 | if self.label: 1082 | s += " label \"{.label}\"".format(self) 1083 | if self.rule_flag & PFRULE_ONCE: 1084 | s += " once" 1085 | 1086 | if self.qname and self.pqname: 1087 | s += " queue({0.qname}, {0.pqname})".format(self) 1088 | elif self.qname: 1089 | s += " queue {.qname}".format(self) 1090 | 1091 | if self.tagname: 1092 | s += " tag {.tagname}".format(self) 1093 | if self.match_tagname: 1094 | if self.match_tag_not: 1095 | s += " !" 1096 | s += " tagged {.match_tagname}".format(self) 1097 | 1098 | if self.rtableid != -1: 1099 | s += " rtable {.rtableid}".format(self) 1100 | 1101 | if self.divert.type != PF_DIVERT_NONE: 1102 | s += " {.divert}".format(self) 1103 | 1104 | if not isinstance(self, PFRuleset): 1105 | if self.nat.addr.type != PF_ADDR_NONE: 1106 | if self.rule_flag & PFRULE_AFTO: 1107 | af = " inet" if (self.naf == AF_INET) else " inet6" 1108 | s += " af-to {} from {.nat}".format(af, self) 1109 | if self.rdr.addr.type != PF_ADDR_NONE: 1110 | s += " to {.rdr}".format(self) 1111 | else: 1112 | s += " nat-to {.nat}".format(self) 1113 | elif self.rdr.addr.type != PF_ADDR_NONE: 1114 | s += " rdr-to {.rdr}".format(self) 1115 | 1116 | if self.rt == PF_ROUTETO: 1117 | s += " route-to {.route}".format(self) 1118 | elif self.rt == PF_REPLYTO: 1119 | s += " reply-to {.route}".format(self) 1120 | elif self.rt == PF_DUPTO: 1121 | s += " dup-to {.route}".format(self) 1122 | 1123 | return s 1124 | 1125 | 1126 | class PFRuleset(PFRule): 1127 | """Class representing a Packet Filter ruleset or anchor.""" 1128 | 1129 | def __init__(self, name="", rule=None, **kw): 1130 | """Check arguments and initialize instance attributes.""" 1131 | self.name = name 1132 | self._altqs = [] 1133 | self._tables = [] 1134 | self._rules = [] 1135 | super(PFRuleset, self).__init__(rule, **kw) 1136 | 1137 | def append(self, *items): 1138 | """Append one or more rules and/or tables and/or altqs.""" 1139 | self._rules.extend(filter(lambda i: isinstance(i, PFRule), items)) 1140 | self._tables.extend(filter(lambda i: isinstance(i, PFTable), items)) 1141 | 1142 | def insert(self, index, rule): 1143 | """Insert a 'rule' before 'index'.""" 1144 | self._rules.insert(index, rule) 1145 | 1146 | def remove(self, index=-1): 1147 | """Remove the rule at 'index'.""" 1148 | self._rules.pop(index) 1149 | 1150 | @property 1151 | def rules(self): 1152 | """Return the rules in this ruleset.""" 1153 | return self._rules 1154 | 1155 | @property 1156 | def tables(self): 1157 | """Return the tables in this ruleset.""" 1158 | return self._tables 1159 | 1160 | def _to_string(self): 1161 | """Return the string representation of the ruleset.""" 1162 | return "\n".join([PFRule._to_string(rule) for rule in self._rules]) 1163 | -------------------------------------------------------------------------------- /pf/state.py: -------------------------------------------------------------------------------- 1 | """Classes representing the entries in the firewall's state table.""" 2 | 3 | from socket import * 4 | from ctypes import * 5 | from struct import * 6 | 7 | from pf.constants import * 8 | from pf._struct import * 9 | from pf._base import PFObject 10 | from pf._utils import getprotobynumber, tcpstates, udpstates 11 | from pf.rule import PFAddr, PFPort 12 | 13 | 14 | __all__ = ['PFStatePeer', 15 | 'PFStateKey', 16 | 'PFState'] 17 | 18 | 19 | class PFStatePeer(PFObject): 20 | """Represents a connection endpoint.""" 21 | 22 | _struct_type = pfsync_state_peer 23 | 24 | def __init__(self, peer): 25 | """Check argument and initialize class attributes.""" 26 | super(PFStatePeer, self).__init__(peer) 27 | 28 | def _from_struct(self, p): 29 | """Initialize class attributes from a pfsync_state_peer structure.""" 30 | self.seqlo = ntohl(p.seqlo) 31 | self.seqhi = ntohl(p.seqhi) 32 | self.seqdiff = ntohl(p.seqdiff) 33 | self.max_win = p.max_win 34 | self.mss = p.mss 35 | self.state = p.state 36 | self.wscale = p.wscale 37 | 38 | self.pfss_flags = p.scrub.pfss_flags 39 | self.pfss_ttl = p.scrub.pfss_ttl 40 | self.scrub_flag = p.scrub.scrub_flag 41 | self.pfss_ts_mod = p.scrub.pfss_ts_mod 42 | 43 | 44 | class PFStateKey(PFObject): 45 | """Represents a state key.""" 46 | 47 | _struct_type = pfsync_state_key 48 | 49 | def __init__(self, key, af): 50 | """Check argument and initialize class attributes.""" 51 | self.af = af 52 | super(PFStateKey, self).__init__(key) 53 | 54 | def _from_struct(self, k): 55 | """Initialize class attributes from a pfsync_state_key structure.""" 56 | a = (pf_addr_wrap(), pf_addr_wrap()) 57 | 58 | 59 | a[0].v.a.addr, a[1].v.a.addr = k.addr 60 | mask = b'\xff' * {AF_INET: 4, AF_INET6: 16}[self.af] 61 | memmove(a[0].v.a.mask.v6, c_char_p(mask), len(mask)) 62 | memmove(a[1].v.a.mask.v6, c_char_p(mask), len(mask)) 63 | 64 | self.addr = (PFAddr(a[0], self.af), PFAddr(a[1], self.af)) 65 | self.port = (PFPort(ntohs(k.port[0])), PFPort(ntohs(k.port[1]))) 66 | self.rdomain = ntohs(k.rdomain) 67 | 68 | 69 | class PFState(PFObject): 70 | """Represents an entry in Packet Filter's state table.""" 71 | 72 | _struct_type = pfsync_state 73 | 74 | def __init__(self, state): 75 | """Check argument and initialize class attributes.""" 76 | super(PFState, self).__init__(state) 77 | 78 | def _from_struct(self, s): 79 | """Initialize class attributes from a pfsync_state structure.""" 80 | self.id = unpack("Q", pack(">Q", s.id))[0] 81 | self.ifname = s.ifname.decode() 82 | 83 | a = pf_addr_wrap() 84 | a.v.a.addr = s.rt_addr 85 | self.rt_addr = PFAddr(a, s.af) 86 | 87 | self.rule = ntohl(s.rule) 88 | self.anchor = ntohl(s.anchor) 89 | self.nat_rule = ntohl(s.nat_rule) 90 | self.creation = ntohl(s.creation) 91 | self.expire = ntohl(s.expire) 92 | 93 | p = unpack('>IIII', string_at(addressof(s.packets), sizeof(s.packets))) 94 | self.packets = ((p[0] << 32 | p[1]), (p[2] << 32 | p[3])) 95 | b = unpack('>IIII', string_at(addressof(s.bytes), sizeof(s.bytes))) 96 | self.bytes = ((b[0] << 32 | b[1]), (b[2] << 32 | b[3])) 97 | 98 | self.creatorid = ntohl(s.creatorid) & 0xffffffff 99 | self.rtableid = s.rtableid 100 | self.max_mss = s.max_mss 101 | self.af = s.af 102 | self.proto = s.proto 103 | self.direction = s.direction 104 | self.log = s.log 105 | self.timeout = s.timeout 106 | self.sync_flags = s.sync_flags 107 | self.updates = s.updates 108 | self.min_ttl = s.min_ttl 109 | self.set_tos = s.set_tos 110 | self.state_flags = s.state_flags 111 | 112 | if self.direction == PF_OUT: 113 | self.src = PFStatePeer(s.src) 114 | self.dst = PFStatePeer(s.dst) 115 | self.sk = PFStateKey(s.key[PF_SK_STACK], s.af) 116 | self.nk = PFStateKey(s.key[PF_SK_WIRE], s.af) 117 | if self.proto in (IPPROTO_ICMP, IPPROTO_ICMPV6): 118 | self.sk.port = (self.nk.port[0], self.sk.port[1]) 119 | else: 120 | self.src = PFStatePeer(s.dst) 121 | self.dst = PFStatePeer(s.src) 122 | self.sk = PFStateKey(s.key[PF_SK_WIRE], s.af) 123 | self.nk = PFStateKey(s.key[PF_SK_STACK], s.af) 124 | if self.proto in (IPPROTO_ICMP, IPPROTO_ICMPV6): 125 | self.sk.port = (self.sk.port[0], self.nk.port[1]) 126 | 127 | def _to_string(self): 128 | """Return a string representing the state.""" 129 | sk, nk = self.sk, self.nk 130 | sp = (sk.port[0].num[0], sk.port[1].num[0]) 131 | np = (nk.port[0].num[0], nk.port[1].num[0]) 132 | src, dst = self.src, self.dst 133 | afto = (nk.af != sk.af) 134 | 135 | s = "{.ifname} ".format(self) 136 | s += "{} ".format(getprotobynumber(self.proto) or self.proto) 137 | 138 | s += "{0.addr[1]}:{0.port[1]}".format(nk) 139 | if afto or (nk.addr[1] != sk.addr[1]) or (np[1] != sp[1]) or \ 140 | (nk.rdomain != sk.rdomain): 141 | i = int(not afto) 142 | s += " ({}:{})".format(sk.addr[i], sk.port[i]) 143 | 144 | if self.direction == PF_OUT or (afto and self.direction == PF_IN): 145 | s += " -> " 146 | else: 147 | s += " <- " 148 | 149 | s += "{0.addr[0]}:{0.port[0]}".format(nk) 150 | if afto or (nk.addr[1] != nk.addr[1]) or (np[1] != sp[1]) or \ 151 | (nk.rdomain != sk.rdomain): 152 | i = int(afto) 153 | s += " ({}:{})".format(sk.addr[i], sk.port[i]) 154 | 155 | s += " " 156 | if self.proto == IPPROTO_TCP: 157 | if (src.state <= TCPS_TIME_WAIT and 158 | dst.state <= TCPS_TIME_WAIT): 159 | s += "{}:{}".format(tcpstates[src.state], 160 | tcpstates[dst.state]) 161 | elif (src.state == PF_TCPS_PROXY_SRC or 162 | dst.state == PF_TCPS_PROXY_SRC): 163 | s += "PROXY:SRC" 164 | elif (src.state == PF_TCPS_PROXY_DST or 165 | dst.state == PF_TCPS_PROXY_DST): 166 | s += "PROXY:DST" 167 | else: 168 | s += "".format(src, dst) 169 | 170 | s += "\n" 171 | for p in src, dst: 172 | s += " [{.seqlo} + {}]".format(p, (p.seqhi - p.seqlo)) 173 | if p.seqdiff: 174 | s += "(+{.seqdiff})".format(p) 175 | if src.wscale and dst.wscale: 176 | s += " wscale {}".format(p.wscale & PF_WSCALE_MASK) 177 | 178 | elif (self.proto == IPPROTO_UDP and 179 | src.state < PFUDPS_NSTATES and dst.state < PFUDPS_NSTATES) or \ 180 | (self.proto not in (IPPROTO_ICMP, IPPROTO_ICMPV6) and 181 | src.state < PFOTHERS_NSTATES and dst.state < PFOTHERS_NSTATES): 182 | s += "{}:{}".format(udpstates[src.state], udpstates[dst.state]) 183 | else: 184 | s += "{.state}:{.state}".format(src, dst) 185 | 186 | hrs, sec = divmod(self.creation, 60) 187 | hrs, min = divmod(hrs, 60) 188 | s += "\n age {:02d}:{:02d}:{:02d}".format(hrs, min, sec) 189 | 190 | hrs, sec = divmod(self.expire, 60) 191 | hrs, min = divmod(hrs, 60) 192 | s += ", expires in {:02d}:{:02d}:{:02d}".format(hrs, min, sec) 193 | 194 | s += ", {0.packets[0]}:{0.packets[1]} pkts".format(self) 195 | s += ", {0.bytes[0]}:{0.bytes[1]} bytes".format(self) 196 | 197 | if self.anchor != 0xffffffff: 198 | s += ", anchor {0.anchor}" .format(self) 199 | if self.rule != 0xffffffff: 200 | s += ", rule {0.rule}".format(self) 201 | if self.state_flags & PFSTATE_SLOPPY: 202 | s += ", sloppy" 203 | if self.state_flags & PFSTATE_PFLOW: 204 | s += ", pflow" 205 | if self.sync_flags & PFSYNC_FLAG_SRCNODE: 206 | s += ", source-track" 207 | if self.sync_flags & PFSYNC_FLAG_NATSRCNODE: 208 | s += ", sticky-address\n" 209 | 210 | s += "\n id: {0.id:016x} creatorid: {0.creatorid:08x}".format(self) 211 | if self.sync_flags & PFSTATE_NOSYNC: 212 | s += " (no-sync)" 213 | 214 | return s 215 | -------------------------------------------------------------------------------- /pf/status.py: -------------------------------------------------------------------------------- 1 | """Classes representing the internal Packet Filter statistics and counters. 2 | 3 | PFStatus objects contain a series of runtime statistical information describing 4 | the current status of the Packet Filter. 5 | """ 6 | 7 | import time 8 | from socket import ntohl 9 | 10 | from pf.constants import * 11 | from pf._struct import pf_status, pfi_kif 12 | from pf._base import PFObject 13 | from pf._utils import dbg_levels, uptime 14 | 15 | 16 | __all__ = ['PFStatus', 17 | 'PFIface'] 18 | 19 | 20 | class PFStatus(PFObject): 21 | """Class representing the internal Packet Filter statistics and counters.""" 22 | 23 | _struct_type = pf_status 24 | 25 | def __init__(self, status): 26 | """Check argument and initialize class attributes.""" 27 | super(PFStatus, self).__init__(status) 28 | 29 | def _from_struct(self, s): 30 | """Initialize class attributes from a pf_status structure.""" 31 | self.ifname = s.ifname.decode() 32 | self.running = bool(s.running) 33 | self.stateid = s.stateid 34 | self.syncookies_inflight = s.syncookies_inflight 35 | self.syncookies_active = s.syncookies_active 36 | self.syncookies_mode = s.syncookies_mode 37 | self.since = s.since 38 | self.states = s.states 39 | self.states_halfopen = s.states_halfopen 40 | self.src_nodes = s.src_nodes 41 | self.debug = s.debug 42 | self.hostid = ntohl(s.hostid) & 0xffffffff 43 | self.reass = s.reass 44 | self.pf_chksum = "0x" + "".join(map("{:02x}".format, s.pf_chksum)) 45 | 46 | self.cnt = {'match': s.counters[0], 47 | 'bad-offset': s.counters[1], 48 | 'fragment': s.counters[2], 49 | 'short': s.counters[3], 50 | 'normalize': s.counters[4], 51 | 'memory': s.counters[5], 52 | 'bad-timestamp': s.counters[6], 53 | 'congestion': s.counters[7], 54 | 'ip-option': s.counters[8], 55 | 'proto-cksum': s.counters[9], 56 | 'state-mismatch': s.counters[10], 57 | 'state-insert': s.counters[11], 58 | 'state-limit': s.counters[12], 59 | 'src-limit': s.counters[13], 60 | 'synproxy': s.counters[14], 61 | 'translate': s.counters[15], 62 | 'no-route': s.counters[16]} 63 | 64 | self.lcnt = {'max states per rule': s.lcounters[0], 65 | 'max-src-states': s.lcounters[1], 66 | 'max-src-nodes': s.lcounters[2], 67 | 'max-src-conn': s.lcounters[3], 68 | 'max-src-conn-rate': s.lcounters[4], 69 | 'overload table insertion': s.lcounters[5], 70 | 'overload flush states': s.lcounters[6], 71 | 'synfloods detected': s.lcounters[7], 72 | 'syncookies sent': s.lcounters[8], 73 | 'syncookies validated': s.lcounters[9]} 74 | 75 | self.fcnt = {'searches': s.fcounters[0], 76 | 'inserts': s.fcounters[1], 77 | 'removals': s.fcounters[2]} 78 | 79 | self.scnt = {'searches': s.scounters[0], 80 | 'inserts': s.scounters[1], 81 | 'removals': s.scounters[2]} 82 | 83 | self.bytes = {'in': (s.bcounters[0][0], s.bcounters[1][0]), 84 | 'out': (s.bcounters[0][1], s.bcounters[1][1])} 85 | 86 | self.packets = {'in': ((s.pcounters[0][0][PF_PASS], 87 | s.pcounters[1][0][PF_PASS]), 88 | (s.pcounters[0][0][PF_DROP], 89 | s.pcounters[1][0][PF_DROP])), 90 | 'out': ((s.pcounters[0][1][PF_PASS], 91 | s.pcounters[1][1][PF_PASS]), 92 | (s.pcounters[0][1][PF_DROP], 93 | s.pcounters[1][1][PF_DROP]))} 94 | 95 | def _to_string(self): 96 | """Return a string containing the statistics.""" 97 | s = "Status: " + ('Enabled' if self.running else 'Disabled') 98 | 99 | if self.since: 100 | runtime = uptime() - self.since 101 | day, sec = divmod(runtime, 60) 102 | day, min = divmod(day, 60) 103 | day, hrs = divmod(day, 24) 104 | s += " for {} days {:02}:{:02}:{:02}".format(day, hrs, min, sec) 105 | 106 | dbg = next((k for k, v in dbg_levels.items() if v == self.debug), 107 | "unknown") 108 | s = "{:<44}{:>15}\n\n".format(s, "Debug: " + dbg) 109 | s += "Hostid: 0x{.hostid:08x}\n".format(self) 110 | s += "Checksum: {.pf_chksum}\n\n".format(self) 111 | 112 | if self.ifname: 113 | fmt = " {0:<25} {1[0]:>14d} {1[1]:>16d}\n" 114 | s += "Interface Stats for {.ifname:<16} ".format(self) 115 | s += "{:>5} {:>16}\n".format("IPv4", "IPv6") 116 | s += fmt.format("Bytes In", self.bytes["in"]) 117 | s += fmt.format("Bytes Out", self.bytes["out"]) 118 | s += " Packets In\n" 119 | s += fmt.format(" Passed", self.packets["in"][PF_PASS]) 120 | s += fmt.format(" Blocked", self.packets["in"][PF_DROP]) 121 | s += " Packets Out\n" 122 | s += fmt.format(" Passed", self.packets["out"][PF_PASS]) 123 | s += fmt.format(" Blocked", self.packets["out"][PF_DROP]) 124 | s += "\n" 125 | 126 | s += "{:<27} {:>14} {:>16}\n".format("State Table", "Total", "Rate") 127 | s += " {:<25} {.states:>14d}\n".format("current entries", self) 128 | s += " {:<25} {.states_halfopen:>14d}".format("half-open tcp", self) 129 | for k, v in self.fcnt.items(): 130 | s += "\n {:<25} {:>14d} ".format(k, v) 131 | if self.since and runtime: 132 | s += "{:>14.1f}/s".format(float(v)/runtime) 133 | 134 | s += "\nSource Tracking Table\n" 135 | s += " {:<25} {.src_nodes:>14d}".format("current entries", self) 136 | for k, v in self.scnt.items(): 137 | s += "\n {:<25} {:>14d} ".format(k, v) 138 | if self.since and runtime: 139 | s += "{:>14.1f}/s".format(float(v)/runtime) 140 | 141 | s += "\nCounters" 142 | for k, v in self.cnt.items(): 143 | s += "\n {:<25} {:>14d} ".format(k, v) 144 | if self.since and runtime: 145 | s += "{:>14.1f}/s".format(float(v)/runtime) 146 | 147 | s += "\nLimit Counters" 148 | for k, v in self.lcnt.items(): 149 | s += "\n {:<25} {:>14d} ".format(k, v) 150 | if self.since and runtime: 151 | s += "{:>14.1f}/s".format(float(v)/runtime) 152 | 153 | return s 154 | 155 | 156 | class PFIface(PFObject): 157 | """Class representing a network interface.""" 158 | 159 | _struct_type = pfi_kif 160 | 161 | def __init__(self, iface): 162 | """Check argument and initialize class attributes.""" 163 | super(PFIface, self).__init__(iface) 164 | 165 | def _from_struct(self, i): 166 | """Initialize class attributes from a pfi_kif structure.""" 167 | self.name = i.pfik_name.decode() 168 | self.packets = {'in': ((i.pfik_packets[0][0][PF_PASS], 169 | i.pfik_packets[1][0][PF_PASS]), 170 | (i.pfik_packets[0][0][PF_DROP], 171 | i.pfik_packets[1][0][PF_DROP])), 172 | 'out': ((i.pfik_packets[0][1][PF_PASS], 173 | i.pfik_packets[1][1][PF_PASS]), 174 | (i.pfik_packets[0][1][PF_DROP], 175 | i.pfik_packets[1][1][PF_DROP]))} 176 | self.bytes = {'in': ((i.pfik_bytes[0][0][PF_PASS], 177 | i.pfik_bytes[1][0][PF_PASS]), 178 | (i.pfik_bytes[0][0][PF_DROP], 179 | i.pfik_bytes[1][0][PF_DROP])), 180 | 'out': ((i.pfik_bytes[0][1][PF_PASS], 181 | i.pfik_bytes[1][1][PF_PASS]), 182 | (i.pfik_bytes[0][1][PF_DROP], 183 | i.pfik_bytes[1][1][PF_DROP]))} 184 | self.flags = i.pfik_flags 185 | self.flags_new = i.pfik_flags_new 186 | self.states = i.pfik_states 187 | self.cleared = i.pfik_tzero 188 | self.rules = i.pfik_rules 189 | self.routes = i.pfik_routes 190 | 191 | def _to_string(self): 192 | """Return a string containing the description of the interface.""" 193 | if self.flags & PFI_IFLAG_SKIP: 194 | s = "{.name} (skip)\n".format(self) 195 | else: 196 | s = "{.name}\n".format(self) 197 | s += "\tCleared: {}\n".format(time.ctime(self.cleared)) 198 | s += "\tReferences: [ States: {.states:<18d}".format(self) 199 | s += " Rules: {.rules:<18d} ]\n".format(self) 200 | 201 | pfik_ops = ("Pass:", "Block:") 202 | for o, p, b in zip(pfik_ops, self.packets["in"], self.bytes["in"]): 203 | l = "\tIn4/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 204 | s += l.format(o, p[0], b[0]) 205 | for o, p, b in zip(pfik_ops, self.packets["out"], self.bytes["out"]): 206 | l = "\tOut4/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 207 | s += l.format(o, p[0], b[0]) 208 | for o, p, b in zip(pfik_ops, self.packets["in"], self.bytes["in"]): 209 | l = "\tIn6/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 210 | s += l.format(o, p[1], b[1]) 211 | for o, p, b in zip(pfik_ops, self.packets["out"], self.bytes["out"]): 212 | l = "\tOut6/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 213 | s += l.format(o, p[1], b[1]) 214 | 215 | return s 216 | -------------------------------------------------------------------------------- /pf/table.py: -------------------------------------------------------------------------------- 1 | """Classes to represent Packet Filter Tables.""" 2 | 3 | import re 4 | import time 5 | from socket import * 6 | from ctypes import * 7 | 8 | from pf.constants import * 9 | from pf._struct import * 10 | from pf._base import PFObject 11 | from pf._utils import ctonm, nmtoc 12 | 13 | 14 | __all__ = ['PFTableAddr', 15 | 'PFTable', 16 | 'PFTStats'] 17 | 18 | 19 | class PFTableAddr(PFObject): 20 | """Represents an address in a PF table.""" 21 | 22 | _struct_type = pfr_addr 23 | 24 | def __init__(self, addr=None, **kw): 25 | """Check argument and initialize class attributes.""" 26 | if addr is None: 27 | addr = pfr_addr() 28 | 29 | super(PFTableAddr, self).__init__(addr, **kw) 30 | 31 | def _from_struct(self, a): 32 | """Initialize class attributes from a pfr_addr structure.""" 33 | l = {AF_INET: 4, AF_INET6: 16}[a.pfra_af] 34 | 35 | self.af = a.pfra_af 36 | self.addr = inet_ntop(self.af, string_at(addressof(a.pfra_u), l)) 37 | self.mask = ctonm(a.pfra_net, self.af) 38 | self.neg = bool(a.pfra_not) 39 | self.fback = a.pfra_fback 40 | self.ifname = a.pfra_ifname.decode() 41 | self.type = a.pfra_type 42 | self.states = a.pfra_states 43 | self.weight = a.pfra_weight 44 | 45 | def _from_string(self, a): 46 | """Initalize a new instance from a string.""" 47 | ipv4_re = "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" 48 | ipv6_re = "[0-9a-f:]+" 49 | addr_re = "(?P!)?\s*" + \ 50 | "(?P
(?P{})|".format(ipv4_re) + \ 51 | "(?P{}))".format(ipv6_re) + \ 52 | "(?:/(?P\d+))?\s*" 53 | 54 | m = re.compile(addr_re).match(a) 55 | if not m: 56 | raise ValueError("Could not parse address: '{}'".format(a)) 57 | 58 | self.neg = bool(m.group("neg")) 59 | 60 | if m.group("ipv4"): 61 | self.af = AF_INET 62 | self.addr = m.group("ipv4") 63 | elif m.group("ipv6"): 64 | self.af = AF_INET6 65 | self.addr = m.group("ipv6") 66 | 67 | net = m.group("mask") or {AF_INET: 32, AF_INET6: 128}[self.af] 68 | self.mask = ctonm(int(net), self.af) 69 | 70 | self.fback = 0 71 | self.ifname = "" # ? 72 | self.type = PFRKE_PLAIN # ? 73 | self.states = 0 74 | self.weight = 0 75 | 76 | def _to_struct(self): 77 | """Convert this instance to a pfr_addr structure.""" 78 | a = pfr_addr() 79 | 80 | addr = inet_pton(self.af, self.addr) 81 | memmove(a.pfra_ip6addr, c_char_p(addr), len(addr)) 82 | 83 | a.pfra_af = self.af 84 | a.pfra_net = nmtoc(self.mask, self.af) 85 | a.pfra_not = int(self.neg) 86 | a.pfra_fback = self.fback 87 | a.pfra_ifname = self.ifname.encode() 88 | a.pfra_type = self.type 89 | 90 | return a 91 | 92 | def _to_string(self): 93 | """Return the string representation of the address.""" 94 | s = ("! {!s}" if self.neg else "{!s}").format(self.addr) 95 | bits = nmtoc(self.mask, self.af) 96 | if not ((self.af == AF_INET and bits == 32) or (bits == 128)): 97 | s += "/{}".format(bits) 98 | 99 | return s 100 | 101 | 102 | class PFTable(PFObject): 103 | """Represents a PF table.""" 104 | 105 | _struct_type = pfr_table 106 | 107 | def __init__(self, table=None, *addrs, **kw): 108 | """Check argument and initialize class attributes.""" 109 | if table is None: 110 | table = pfr_table() 111 | elif isinstance(table, str): 112 | table = pfr_table(pfrt_name=table.encode()) 113 | 114 | self._addrs = [] 115 | for addr in addrs: 116 | if not isinstance(addr, PFTableAddr): 117 | addr = PFTableAddr(addr) 118 | self._addrs.append(addr) 119 | 120 | super(PFTable, self).__init__(table, **kw) 121 | 122 | @property 123 | def addrs(self): 124 | """Return a tuple containing the address in the table.""" 125 | return tuple(self._addrs) 126 | 127 | def _from_struct(self, t): 128 | """Initialize class attributes from a pfr_table structure""" 129 | self.anchor = t.pfrt_anchor.decode() 130 | self.name = t.pfrt_name.decode() 131 | self.flags = t.pfrt_flags 132 | self.fback = t.pfrt_fback 133 | 134 | def _to_struct(self): 135 | """Convert this instance to a pfr_table structure.""" 136 | t = pfr_table() 137 | 138 | t.pfrt_anchor = self.anchor.encode() 139 | t.pfrt_name = self.name.encode() 140 | t.pfrt_flags = self.flags & (PFR_TFLAG_CONST|PFR_TFLAG_PERSIST) 141 | t.pfrt_fback = self.fback 142 | 143 | return t 144 | 145 | def _to_string(self): 146 | """Return the string representation of the table.""" 147 | s = ('c' if (self.flags & PFR_TFLAG_CONST) else '-') 148 | s += ('p' if (self.flags & PFR_TFLAG_PERSIST) else '-') 149 | s += ('a' if (self.flags & PFR_TFLAG_ACTIVE) else '-') 150 | s += ('i' if (self.flags & PFR_TFLAG_INACTIVE) else '-') 151 | s += ('r' if (self.flags & PFR_TFLAG_REFERENCED) else '-') 152 | s += ('h' if (self.flags & PFR_TFLAG_REFDANCHOR) else '-') 153 | s += ('C' if (self.flags & PFR_TFLAG_COUNTERS) else '-') 154 | s += "\t{.name}".format(self) 155 | 156 | if self.anchor: 157 | s += "\t{.anchor}".format(self) 158 | 159 | return s 160 | 161 | 162 | class PFTStats(PFObject): 163 | """Class containing statistics for a PF table.""" 164 | 165 | _struct_type = pfr_tstats 166 | 167 | def __init__(self, tstats): 168 | """Initialize class attributes.""" 169 | super(PFTStats, self).__init__(tstats) 170 | 171 | def _from_struct(self, s): 172 | """Initialize class attributes from a pfr_tstats structure.""" 173 | self.table = PFTable(s.pfrts_t) 174 | self.packets = {"in": tuple(s.pfrts_packets[PFR_DIR_IN]), 175 | "out": tuple(s.pfrts_packets[PFR_DIR_OUT])} 176 | self.bytes = {"in": tuple(s.pfrts_bytes[PFR_DIR_IN]), 177 | "out": tuple(s.pfrts_bytes[PFR_DIR_OUT])} 178 | self.cleared = s.pfrts_tzero 179 | self.cnt = s.pfrts_cnt 180 | self.evalcnt = {"match": s.pfrts_match, 181 | "nomatch": s.pfrts_nomatch} 182 | self.refcnt = {"rules": s.pfrts_refcnt[PFR_REFCNT_RULE], 183 | "anchors": s.pfrts_refcnt[PFR_REFCNT_ANCHOR]} 184 | 185 | def _to_string(self): 186 | """Return the string representation of the table statistics.""" 187 | s = "{.table}\n".format(self) 188 | s += "\tAddresses: {.cnt:d}\n".format(self) 189 | s += "\tCleared: {}\n".format(time.ctime(self.cleared)) 190 | s += "\tReferences: [ Anchors: {anchors:<18d} Rules: {rules:<18d} ]\n" 191 | s += "\tEvaluations: [ NoMatch: {nomatch:<18d} Match: {match:<18d} ]\n" 192 | s = s.format(**dict(self.refcnt, **self.evalcnt)) 193 | 194 | pfr_ops = ("Block:", "Pass:", "XPass:") 195 | for o, p, b in zip(pfr_ops, self.packets["in"], self.bytes["in"]): 196 | l = "\tIn/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 197 | s += l.format(o, p, b) 198 | for o, p, b in zip(pfr_ops, self.packets["out"], self.bytes["out"]): 199 | l = "\tOut/{:<6s} [ Packets: {:<18d} Bytes: {:<18d} ]\n" 200 | s += l.format(o, p, b) 201 | 202 | return s.rstrip() 203 | -------------------------------------------------------------------------------- /pf/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test module for pf""" 2 | 3 | from pf.tests.cmd import TestCommand 4 | from pf.tests.test_filter import TestPacketFilter 5 | 6 | 7 | __all__ = ["TestCommand", 8 | "TestPacketFilter"] 9 | -------------------------------------------------------------------------------- /pf/tests/cmd.py: -------------------------------------------------------------------------------- 1 | """Command for running unit tests for the module from setup.py""" 2 | 3 | from distutils.cmd import Command 4 | from unittest import TextTestRunner, TestLoader 5 | 6 | import pf.tests 7 | 8 | 9 | class TestCommand(Command): 10 | """Command for running unit tests for the module from setup.py""" 11 | 12 | user_options = [] 13 | 14 | def initialize_options(self): 15 | pass 16 | 17 | def finalize_options(self): 18 | pass 19 | 20 | def run(self): 21 | suite = TestLoader().loadTestsFromModule(pf.tests) 22 | TextTestRunner(verbosity=1).run(suite) 23 | -------------------------------------------------------------------------------- /pf/tests/test_filter.py: -------------------------------------------------------------------------------- 1 | """Test suite for the PacketFilter class""" 2 | 3 | import os 4 | import time 5 | import unittest 6 | import subprocess 7 | from socket import * 8 | 9 | import pf 10 | 11 | 12 | class TestPacketFilter(unittest.TestCase): 13 | """Test case class for pf.PacketFilter""" 14 | 15 | testif = "lo0" 16 | 17 | def setUp(self): 18 | self.pf = pf.PacketFilter() 19 | self._init_state = {"enabled": self.pf.get_status().running, 20 | "ruleset": self.pf.get_ruleset()} 21 | if not self._init_state["enabled"]: 22 | self.pf.enable() 23 | self.pf.clear_rules() 24 | 25 | def tearDown(self): 26 | self.pf.clear_rules() 27 | if not self._init_state["enabled"]: 28 | self.pf.disable() 29 | else: 30 | self.pf.load_ruleset(self._init_state["ruleset"]) 31 | 32 | def test_enable(self): 33 | self.pf.disable() 34 | self.assertFalse(self.pf.get_status().running) 35 | self.pf.enable() 36 | self.assertTrue(self.pf.get_status().running) 37 | 38 | def test_set_debug(self): 39 | _dbg = self.pf.get_status().debug 40 | for dbg in (pf.LOG_DEBUG, pf.LOG_ERR, _dbg): 41 | self.pf.set_debug(dbg) 42 | self.assertEqual(self.pf.get_status().debug , dbg) 43 | 44 | def test_set_hostid(self): 45 | _id = self.pf.get_status().hostid 46 | for id in (1234, _id): 47 | self.pf.set_hostid(id) 48 | self.assertEqual(self.pf.get_status().hostid , id) 49 | 50 | def test_set_reassembly(self): 51 | _flags = self.pf.get_status().reass 52 | flags = pf.PF_REASS_ENABLED | pf.PF_REASS_NODF 53 | self.pf.set_reassembly(flags) 54 | self.assertEqual(self.pf.get_status().reass, flags) 55 | self.pf.set_reassembly(_flags) 56 | 57 | def test_set_limit(self): 58 | limits = {"states": 5000, "tables": 2000} 59 | for limit, value in limits.items(): 60 | _value = self.pf.get_limit(limit) 61 | self.pf.set_limit(limit, value) 62 | self.assertEqual(self.pf.get_limit(limit), value) 63 | self.pf.set_limit(limit, _value) 64 | 65 | def test_set_timeout(self): 66 | timeouts = {"frag": 25, "interval": 20} 67 | for tmout, value in timeouts.items(): 68 | _value = self.pf.get_timeout(tmout) 69 | self.pf.set_timeout(tmout, value) 70 | self.assertEqual(self.pf.get_timeout(tmout), value) 71 | self.pf.set_timeout(tmout, _value) 72 | 73 | def test_set_ifflags(self): 74 | _flags = self.pf.get_ifaces(self.testif).flags 75 | self.pf.clear_ifflags(self.testif) 76 | self.pf.set_ifflags(self.testif, pf.PFI_IFLAG_SKIP) 77 | self.assertEqual(self.pf.get_ifaces(self.testif).flags, 78 | pf.PFI_IFLAG_SKIP) 79 | self.pf.set_ifflags(self.testif, _flags) 80 | 81 | def test_set_status_if(self): 82 | _ifname = self.pf.get_status().ifname 83 | self.pf.set_status_if(self.testif) 84 | self.assertEqual(self.pf.get_status().ifname, self.testif) 85 | self.pf.set_status_if(_ifname) 86 | 87 | def test_clear_status(self): 88 | self.pf.clear_status() 89 | self.assertGreaterEqual(pf._utils.uptime(), self.pf.get_status().since) 90 | 91 | def __test_clear_states(self): 92 | self.pf.clear_rules() 93 | self._create_state() 94 | self.assertIsNotNone(filter(lambda s: s.proto == IPPROTO_UDP, 95 | self.pf.get_states())) 96 | self.pf.clear_states() 97 | for state in self.pf.get_states(): 98 | self.assertNotEqual(state.proto, IPPROTO_UDP) 99 | 100 | def __test_kill_states(self): 101 | self.pf.clear_rules() 102 | self._create_state() 103 | self.assertEqual(self.pf.kill_states(proto=IPPROTO_UDP), 1) 104 | 105 | def test_clear_rules(self): 106 | ruleset = pf.PFRuleset() 107 | ruleset.append(pf.PFRule(action=pf.PF_PASS, 108 | flags="S", flagset="SA", 109 | keep_state=pf.PF_STATE_NORMAL)) 110 | self.pf.load_ruleset(ruleset) 111 | self.pf.clear_rules() 112 | self.assertFalse(self.pf.get_ruleset().rules) 113 | 114 | def test_load_queues(self): 115 | # Rules to load in pf.conf format: 116 | # queue std on em0 bandwidth 100M 117 | # queue ssh parent std bandwidth 10M burst 90M for 100ms 118 | # queue mail parent std bandwidth 10M, min 5M, max 25M 119 | # queue http parent std bandwidth 80M default 120 | ifname = self.testif 121 | parentq = "root_" + ifname 122 | MB = 10**6 123 | queues = [pf.PFQueue(qname=parentq, ifname=ifname, 124 | flags=pf.PFQS_ROOTCLASS, 125 | linkshare=pf.ServiceCurve(bandwidth=100*MB)), 126 | pf.PFQueue(qname="std", parent=parentq, ifname=ifname, 127 | linkshare=pf.ServiceCurve(bandwidth=100*MB)), 128 | pf.PFQueue(qname="ssh", parent="std", ifname=ifname, 129 | linkshare=pf.ServiceCurve(10*MB, 90*MB, 100)), 130 | pf.PFQueue(qname="mail", parent="std", ifname=ifname, 131 | linkshare=pf.ServiceCurve(bandwidth=10*MB), 132 | realtime=pf.ServiceCurve(bandwidth=5*MB), 133 | upperlimit=pf.ServiceCurve(bandwidth=25*MB)), 134 | pf.PFQueue(qname="http", parent="std", ifname=ifname, 135 | linkshare=pf.ServiceCurve(bandwidth=80*MB), 136 | flags=pf.PFQS_DEFAULT)] 137 | self.pf.clear_rules() 138 | self.pf.load_queues(*queues) 139 | self.assertEqual(len(self.pf.get_queues()), len(queues)) 140 | for queue in self.pf.get_queues(): 141 | self.assertIn(queue.qname, map(lambda q: q.qname, queues)) 142 | 143 | def test_load_ruleset(self): 144 | iface = pf.PFAddr(type=pf.PF_ADDR_DYNIFTL, ifname=self.testif) 145 | tables = [pf.PFTable("web_srv", "10.0.1.20", "10.0.1.21", "10.0.1.22")] 146 | rules = [ 147 | # match out on $ifname inet from !($ifname) to any nat-to ($ifname) 148 | pf.PFRule(action=pf.PF_MATCH, 149 | direction=pf.PF_OUT, 150 | ifname=self.testif, 151 | af=AF_INET, 152 | src=pf.PFRuleAddr(iface, neg=True), 153 | nat=pf.PFPool(pf.PF_POOL_NAT, iface)), 154 | # pass out quick 155 | pf.PFRule(action=pf.PF_PASS, 156 | direction=pf.PF_OUT, 157 | quick=True, 158 | flags="S", flagset="SA", 159 | keep_state=pf.PF_STATE_NORMAL), 160 | # anchor "test_anchor" 161 | pf.PFRuleset("test_anchor"), 162 | # pass in on $ifname inet proto tcp from any to $ifname port ssh 163 | pf.PFRule(action=pf.PF_PASS, 164 | direction=pf.PF_IN, 165 | ifname=self.testif, 166 | af=AF_INET, 167 | proto=IPPROTO_TCP, 168 | dst=pf.PFRuleAddr(iface, pf.PFPort("ssh", IPPROTO_TCP)), 169 | flags="S", flagset="SA", 170 | keep_state=pf.PF_STATE_NORMAL), 171 | # pass in on $ifname inet proto tcp to $ifname port www \ 172 | # rdr-to round-robin sticky-address 173 | pf.PFRule(action=pf.PF_PASS, 174 | direction=pf.PF_IN, 175 | ifname=self.testif, 176 | af=AF_INET, 177 | proto=IPPROTO_TCP, 178 | dst=pf.PFRuleAddr(iface, pf.PFPort("www", IPPROTO_TCP)), 179 | flags="S", flagset="SA", 180 | keep_state=pf.PF_STATE_NORMAL, 181 | rdr=pf.PFPool(pf.PF_POOL_RDR, pf.PFAddr(""), 182 | opts=(pf.PF_POOL_ROUNDROBIN| 183 | pf.PF_POOL_STICKYADDR))), 184 | # pass out on $ifname inet proto tcp to port 80 \ 185 | # divert-packet port 700 186 | pf.PFRule(action=pf.PF_PASS, 187 | direction=pf.PF_OUT, 188 | ifname=self.testif, 189 | af=AF_INET, 190 | proto=IPPROTO_TCP, 191 | dst=pf.PFRuleAddr(port=pf.PFPort("www", IPPROTO_TCP)), 192 | divert=pf.PFDivert(pf.PF_DIVERT_PACKET, port=700)), 193 | # pass in inet proto icmp all icmp-type echoreq max-pkt-rate 100/10 194 | pf.PFRule(action=pf.PF_PASS, 195 | direction=pf.PF_IN, 196 | af=AF_INET, 197 | proto=IPPROTO_ICMP, 198 | type=pf.ICMP_ECHO+1, 199 | keep_state=pf.PF_STATE_NORMAL, 200 | pktrate=pf.PFThreshold(100, 10))] 201 | 202 | rules[2].append( 203 | pf.PFTable("spammers", flags=pf.PFR_TFLAG_PERSIST), 204 | # pass in on $ifname inet proto tcp from ! \ 205 | # to $ifname port 25 rdr-to 10.0.1.23 206 | pf.PFRule(action=pf.PF_PASS, 207 | direction=pf.PF_IN, 208 | ifname=self.testif, 209 | af=AF_INET, 210 | proto=IPPROTO_TCP, 211 | src=pf.PFRuleAddr(pf.PFAddr(""), neg=True), 212 | dst=pf.PFRuleAddr(iface, pf.PFPort(25, IPPROTO_TCP)), 213 | flags="S", flagset="SA", 214 | keep_state=pf.PF_STATE_NORMAL, 215 | rdr=pf.PFPool(pf.PF_POOL_RDR, pf.PFAddr("10.0.1.23")))) 216 | 217 | self.pf.clear_rules() 218 | rs = pf.PFRuleset() 219 | rs.append(*tables) 220 | rs.append(*rules) 221 | self.pf.load_ruleset(rs) 222 | self.assertEqual(len(self.pf.get_ruleset().rules), len(rules)) 223 | 224 | def test_set_syncookies(self): 225 | modes = (pf.PF_SYNCOOKIES_NEVER, "always") 226 | # As far as I know, there's no way to retrieve the current syncookies 227 | # mode from the system. 228 | # So I assume that if no exception was raised, everything is OK. 229 | for mode in modes: 230 | self.pf.set_syncookies(mode) 231 | 232 | def test_set_synflood_watermarks(self): 233 | hiwat, lowat = 2000, 1000 234 | _hiwat, _lowat = self.pf.get_synflood_watermarks() 235 | self.pf.set_synflood_watermarks(hiwat, lowat) 236 | self.assertEqual(self.pf.get_synflood_watermarks(), (hiwat, lowat)) 237 | self.pf.set_synflood_watermarks(_hiwat, _lowat) 238 | 239 | def test_add_tables(self): 240 | tblname = "test_table" 241 | self.assertEqual(self._add_table(tblname), 1) 242 | self.assertEqual(self.pf.get_tables()[0].name, tblname) 243 | 244 | def test_clear_tables(self): 245 | self.assertEqual(self._add_table(), 1) 246 | self.pf.clear_tables() 247 | self.assertFalse(self.pf.get_tables()) 248 | 249 | def test_del_tables(self): 250 | self.assertEqual(self._add_table(), 1) 251 | table = self.pf.get_tables()[0] 252 | self.pf.del_tables(table) 253 | self.assertFalse(self.pf.get_tables()) 254 | 255 | def test_test_addrs(self): 256 | self.assertEqual(self._add_table(), 1) 257 | table = self.pf.get_tables()[0] 258 | self.assertTrue(self.pf.test_addrs(table, "10.0.1.10")) 259 | self.assertFalse(self.pf.test_addrs(table, "10.0.1.20")) 260 | 261 | def test_add_addrs(self): 262 | self.assertEqual(self._add_table(), 1) 263 | table = self.pf.get_tables()[0] 264 | addrs = ["10.0.1.11", "10.0.1.12"] 265 | self.pf.clear_addrs(table) 266 | self.assertEqual(self.pf.add_addrs(table, *addrs), len(addrs)) 267 | self.assertEqual(len(self.pf.get_addrs(table)), len(addrs)) 268 | 269 | def test_clear_addrs(self): 270 | self.assertEqual(self._add_table(), 1) 271 | table = self.pf.get_tables()[0] 272 | self.assertTrue(self.pf.clear_addrs(table)) 273 | self.assertFalse(self.pf.get_addrs(table)) 274 | 275 | def test_del_addrs(self): 276 | self.assertEqual(self._add_table(), 1) 277 | table = self.pf.get_tables()[0] 278 | addrs = ["10.0.1.11", "10.0.1.12"] 279 | self.assertEqual(self.pf.set_addrs(table, *addrs)[1], len(addrs)) 280 | self.assertEqual(self.pf.del_addrs(table, addrs.pop()), 1) 281 | self.assertEqual(len(self.pf.get_addrs(table)), len(addrs)) 282 | 283 | def test_set_addrs(self): 284 | self.assertEqual(self._add_table(), 1) 285 | table = self.pf.get_tables()[0] 286 | addrs = ["10.0.1.11", "10.0.1.12"] 287 | self.assertEqual(self.pf.set_addrs(table, *addrs)[1], len(addrs)) 288 | self.assertEqual(len(self.pf.get_addrs(table)), len(addrs)) 289 | 290 | def test_get_tstats(self): 291 | self.assertEqual(self._add_table(), 1) 292 | self.assertTrue(self.pf.get_tstats()) 293 | 294 | def test_clear_tstats(self): 295 | self.assertEqual(self._add_table(), 1) 296 | table = self.pf.get_tables()[0] 297 | self.assertEqual(self.pf.clear_tstats(table), 1) 298 | 299 | def _add_table(self, tblname="test_table"): 300 | table = pf.PFTable(tblname, "10.0.1.10", 301 | flags=pf.PFR_TFLAG_PERSIST) 302 | self.pf.clear_tables() 303 | return self.pf.add_tables(table) 304 | 305 | def _create_state(self): 306 | ruleset = pf.PFRuleset() 307 | ruleset.append(pf.PFRule(action=pf.PF_PASS, 308 | flags="S", flagset="SA", 309 | proto=IPPROTO_UDP, 310 | keep_state=pf.PF_STATE_NORMAL)) 311 | self.pf.load_ruleset(ruleset) 312 | self.pf.clear_states() 313 | with open(os.devnull, "w") as n: 314 | subprocess.call(["/usr/sbin/nslookup", "google.com"], 315 | stdout=n, stderr=n) 316 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This is the setup script of py-pf. You can install the module by running it with 5 | the 'install' command: 6 | 7 | # python setup.py install 8 | 9 | or run unit tests by calling it with the 'test' command: 10 | 11 | # python setup.py install 12 | """ 13 | 14 | from distutils.core import setup 15 | from pf.tests import TestCommand 16 | 17 | 18 | __author__ = "Daniele Mazzocchio " 19 | __version__ = "0.2.3" 20 | __date__ = "Dec 22, 2023" 21 | 22 | 23 | setup(name = "py-pf", 24 | version = __version__, 25 | author = "Daniele Mazzocchio", 26 | author_email = "danix@kernel-panic.it", 27 | url = "http://www.kernel-panic.it/software/py-pf/", 28 | download_url = "https://github.com/dotpy/py-pf/archive/refs/tags/{}.tar.gz".format(__version__), 29 | packages = ["pf", "pf.tests"], 30 | cmdclass = {"test": TestCommand}, 31 | license = "OSI-Approved :: BSD License", 32 | description = "Pure-Python module for managing OpenBSD's Packet Filter", 33 | classifiers = ["Development Status :: 4 - Beta", 34 | "Intended Audience :: System Administrators", 35 | "License :: OSI Approved :: BSD License", 36 | "Natural Language :: English", 37 | "Operating System :: POSIX :: BSD :: OpenBSD", 38 | "Programming Language :: Python", 39 | "Topic :: System :: Networking :: Firewalls"]) 40 | --------------------------------------------------------------------------------