├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── README.rst ├── examples └── simple_stateful.py ├── pyproject.toml ├── pyptables ├── __init__.py ├── __main__.py ├── base.py ├── chains.py ├── rules │ ├── __init__.py │ ├── arguments.py │ ├── base.py │ ├── forwarding │ │ ├── __init__.py │ │ ├── base.py │ │ ├── channels.py │ │ ├── hosts.py │ │ ├── ipsets.py │ │ ├── locations.py │ │ └── zones.py │ ├── input │ │ ├── __init__.py │ │ └── base.py │ ├── marks.py │ └── matches.py ├── tables.py └── test │ ├── __init__.py │ └── test.dat ├── setup.cfg └── test /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | .idea/ 57 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v1.0 - 26 April 2014 -- Initial release. 2 | v1.0.4 - Python 3 support 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | CHANGES.txt 3 | LICENSE.txt 4 | README.rst 5 | setup.py 6 | pyptables/__init__.py 7 | pyptables/__main__.py 8 | pyptables/base.py 9 | pyptables/chains.py 10 | pyptables/tables.py 11 | pyptables/rules/__init__.py 12 | pyptables/rules/arguments.py 13 | pyptables/rules/base.py 14 | pyptables/rules/marks.py 15 | pyptables/rules/matches.py 16 | pyptables/rules/forwarding/__init__.py 17 | pyptables/rules/forwarding/base.py 18 | pyptables/rules/forwarding/channels.py 19 | pyptables/rules/forwarding/hosts.py 20 | pyptables/rules/forwarding/ipsets.py 21 | pyptables/rules/forwarding/locations.py 22 | pyptables/rules/forwarding/zones.py 23 | pyptables/rules/input/__init__.py 24 | pyptables/rules/input/base.py 25 | pyptables/test/__init__.py 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | recursive-include docs *.txt 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | python-pyptables 3 | ================ 4 | 5 | Python package for generating Linux iptables configurations. 6 | 7 | ************** 8 | About Iptables 9 | ************** 10 | 11 | Iptables is part of the Linux kernel, and is responsible for network packet filtering and manipulation. It is commonly used for building Linux-based firewalls. As packets traverse the Linux network stack, the kernel uses the rules defined in iptables decide what to do with the packet. 12 | 13 | Using iptables involves configuring the rules that are contained in iptables. Each table is composed of chains of rules. Chains come in two flavours: built-in and user-defined. A built-in chain is an entry point into the iptables rule set that is consulted by the kernel when packet reaches at a certain point in the Linux networking stack. For example, the ``tables['filter']['OUTPUT']`` chain is consulted when a local process on the machine generates an outgoing packet.and each table/chain is consulted at different points in the network stack. User-defined chains are only consulted if called from one of the built-in chains (or from another user chain the is called from a built-in chain). 14 | 15 | Chains are then made up of an ordered set of rules. A rule is composed of a set matching parameters (e.g. protocol, destination IP address/port, and many more), and an action (e.g. allow, drop, reject, log, modify the packet). When ever a packet matches a rule, the corresponding action is taken. 16 | 17 | *************** 18 | About PyPTables 19 | *************** 20 | 21 | PyPTables is a python package to allow the generation of a set of iptables rules from a python script. 22 | 23 | Basic usage 24 | =========== 25 | 26 | The following code will create a simple set of rules for a stateful firewall allowing only HTTP, HTTPS and DNS traffic to be routed though the box: 27 | 28 | :: 29 | 30 | from pyptables import default_tables, restore 31 | from pyptables.rules import Rule, Accept 32 | 33 | # get a default set of tables and chains 34 | tables = default_tables() 35 | 36 | # get the forward chain of the filter tables 37 | forward = tables['filter']['FORWARD'] 38 | 39 | # any packet matching an established connection should be allowed 40 | forward.append(Accept(match='conntrack', ctstate='ESTABLISHED') 41 | 42 | # add rules to the forward chain for DNS, HTTP and HTTPS ports 43 | forward.append(Accept(proto='tcp', dport='53')) 44 | forward.append(Accept(proto='tcp', dport='80')) 45 | forward.append(Accept(proto='tcp', dport='443')) 46 | 47 | # any packet not matching a rules will be dropped 48 | forward.policy = Rule.DROP 49 | 50 | Rules in this case are added to the iptables ``filter`` table (for packet filtering), in the ``FORWARD`` chain (for routed or bridged packets, going to and from external sources). 51 | 52 | You can write the resulting rules into the kernel with the restore function: 53 | 54 | :: 55 | 56 | restore(tables) 57 | 58 | Or you can use the ``tables.to_iptables()`` function to generate the resulting iptables commands as a string. 59 | 60 | Tables 61 | ====== 62 | 63 | The top-level container in PyPTables is the ``Tables`` class, which represents a collection of iptables (i.e. filter, mangle, nat). For the most part, you will want to start with a call to ``default_tables()``, which will create a basic structure of tables and chains that represent the built-in tables and chains available in the Linux kernel. 64 | 65 | ``Tables`` is a dictionary-like structure, and is indexable by table name using the ``[]`` operator: 66 | 67 | :: 68 | 69 | tables = default_tables() 70 | table = tables['filter'] 71 | 72 | An individual table is represented by the ``Table`` class, with contains a collection of chains (i.e. INPUT, OUTPUT, FORWARD). This is also a dictionary-like structure, and is indexable by chain name using the ``[]`` operator: 73 | 74 | :: 75 | 76 | chain = tables['filter']['INPUT'] 77 | 78 | Chains 79 | ====== 80 | 81 | Chains hold an ordered list of rules. As mentioned earlier, chains come in two flavours: built-in and user. In PyPTables, these are represented by the ``BuiltinChain`` and ``UserChain`` classes respectively. The only difference between ``BuiltinChain`` and ``UserChain`` chain is that a ``BuiltinChain`` has as default policy, which is enacted when no rule in the chain has matched and dealt with the packet. 82 | 83 | The ``Chain`` classes are list-like structures, and most standard python list operations can be used on them (i.e. ``append(rule)``, ``remove(rule)``, ``insert(rule, position)``) for example: 84 | 85 | :: 86 | 87 | tables['filter']['INPUT'].append(Rule(...)) 88 | tables['filter']['INPUT'].insert(Rule(...), 0) 89 | 90 | For illustration of how the ``Tables``, ``Table`` and ``BuiltinChain`` classes are used, here is the code that implements ``default_tables()``: 91 | 92 | :: 93 | 94 | def default_tables(): 95 | """Generate a set of iptables containing all the default tables and chains""" 96 | 97 | return Tables(Table('filter', 98 | BuiltinChain('INPUT', 'ACCEPT'), 99 | BuiltinChain('FORWARD', 'ACCEPT'), 100 | BuiltinChain('OUTPUT', 'ACCEPT'), 101 | ), 102 | Table('nat', 103 | BuiltinChain('PREROUTING', 'ACCEPT'), 104 | BuiltinChain('OUTPUT', 'ACCEPT'), 105 | BuiltinChain('POSTROUTING', 'ACCEPT'), 106 | ), 107 | Table('mangle', 108 | BuiltinChain('PREROUTING', 'ACCEPT'), 109 | BuiltinChain('INPUT', 'ACCEPT'), 110 | BuiltinChain('FORWARD', 'ACCEPT'), 111 | BuiltinChain('OUTPUT', 'ACCEPT'), 112 | BuiltinChain('POSTROUTING', 'ACCEPT'), 113 | ), 114 | ) 115 | 116 | You can of course choose not to use the ``default_tables()`` function, and create the basic tables structure yourself. This would be needed if for example you want to use ip6tables, or use non-standard tables. 117 | 118 | User chains 119 | ----------- 120 | 121 | The ``UserChain`` class can be used to define user-defined chains. 122 | 123 | :: 124 | 125 | chain = UserChain('test_chain', comment='A user chain') 126 | chain.append(Rule(i='eth0', s='1.1.2.1', d__not='1.1.1.2', jump='DROP', comment='A Rule')) 127 | 128 | User-defined chains can be referenced to from the built-in chain via a jump (and others similar constructs). 129 | 130 | :: 131 | 132 | tables = default_tables() 133 | tables['filter'].append(chain) 134 | tables['filter']['INPUT'].append(Jump(chain)) 135 | 136 | Rules 137 | ===== 138 | 139 | The ``Rule`` class represents an actual iptables rule. Rules are created using a simple, pythonic syntax, and can then be added to a chain. For example, the following call will produce a rule which matches traffic destined for tcp port 22 (SSH) and rejects it: 140 | 141 | :: 142 | 143 | reject_ssh = Rule(proto='tcp', dport='22', jump='REJECT') 144 | 145 | We can then add that to the INPUT chain of the filter tables, to prevent access to SSH port on the local machine. 146 | 147 | :: 148 | 149 | tables['filter']['INPUT'].append(reject_ssh) 150 | 151 | This would result in the following iptables commands being produced: 152 | 153 | :: 154 | 155 | * filter 156 | ... 157 | -A INPUT -p tcp -j REJECT --dport 22 158 | ... 159 | 160 | There are various types of rule already defined that provide defaults for various common parameters. For example, the common jump targets (ACCEPT, DROP, REJECT, etc) already have handy predefined rules with the ``jump`` parameter already set. Using these above could be written: 161 | 162 | :: 163 | 164 | from pyptables.rules import Reject 165 | reject_ssh = Reject(proto='tcp', dport='22') 166 | 167 | You can define new types of rule yourself, for example, you could create an SSH type for matching SSH packets, and use it in various ways: 168 | 169 | :: 170 | 171 | SSH = Rule(proto='tcp', dport='22') 172 | tables['filter']['INPUT'].append(SSH(jump='ACCEPT', source='1.1.1.1', comment='Allow SSH from my workstation')) 173 | tables['filter']['INPUT'].append(SSH(jump='REJECT', comment='Prevent any other access to local SSH')) 174 | tables['filter']['FORWARD'].append(SSH(jump='REJECT', comment='Don't route any SSH traffic ')) 175 | 176 | This would result in the following iptables configuration being generated: 177 | 178 | :: 179 | 180 | ############################################################################### 181 | # filter table (/blocker/share/python/iptables/__init__.py:14 default_tables) # 182 | ############################################################################### 183 | *filter 184 | :INPUT ACCEPT [0:0] 185 | :FORWARD ACCEPT [0:0] 186 | :OUTPUT ACCEPT [0:0] 187 | 188 | # Builtin Chain "INPUT" (/blocker/share/python/iptables/__init__.py:12 default_tables)" 189 | # Rule: Allow access to local SSH from my workstation (:1 ) 190 | -A INPUT -p tcp -s 1.1.1.1 -j ACCEPT --dport 22 -m comment --comment "Allow SSH from my workstation" 191 | # Rule: Prevent any other access to local SSH (:1 ) 192 | -A INPUT -p tcp -j REJECT --dport 22 -m comment --comment "Prevent any other access to local SSH" 193 | 194 | # Builtin Chain "FORWARD" (/blocker/share/python/iptables/__init__.py:13 default_tables)" 195 | # Rule: Prevent any SSH traffic being routed through this box (:1 ) 196 | -A FORWARD -p tcp -j REJECT --dport 22 -m comment --comment "Don't route any SSH traffic" 197 | 198 | # Builtin Chain "OUTPUT" (/blocker/share/python/iptables/__init__.py:14 default_tables)" 199 | # No rules 200 | 201 | Higher-Level Rules 202 | ================== 203 | 204 | TODO 205 | 206 | ***** 207 | Build 208 | ***** 209 | 210 | :: 211 | 212 | cd ~/sources/python-pyptables/ 213 | python3 -m venv ~/build/ 214 | . ~/build/bin/activate 215 | pip install --upgrade build twine 216 | python -m build 217 | twine upload dist/* 218 | 219 | 220 | *********** 221 | Issues/Bugs 222 | *********** 223 | 224 | Any issues or bug reports, please contact `jamie_cockburn@hotmail.co.uk `_ 225 | -------------------------------------------------------------------------------- /examples/simple_stateful.py: -------------------------------------------------------------------------------- 1 | from pyptables import default_tables, restore 2 | from pyptables.rules import Rule, Accept 3 | 4 | # get a default set of tables and chains 5 | tables = default_tables() 6 | 7 | # get the forward chain of the filter tables 8 | forward = tables['filter']['FORWARD'] 9 | 10 | # any packet matching an established connection should be allowed 11 | forward.append(Accept(match='conntrack', ctstate='ESTABLISHED')) 12 | 13 | # add rules to the forward chain for DNS, HTTP and HTTPS ports 14 | forward.append(Accept(proto='tcp', dport='53')) 15 | forward.append(Accept(proto='tcp', dport='80')) 16 | forward.append(Accept(proto='tcp', dport='443')) 17 | 18 | # any packet not matching a rules will be dropped 19 | forward.policy = Rule.DROP 20 | 21 | # write the rules into the kernel 22 | restore(tables) 23 | 24 | print(tables.to_iptables()) 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # Minimum requirements for the build system to execute. 3 | requires = ["setuptools", "wheel"] 4 | build-backend = "setuptools.build_meta" 5 | -------------------------------------------------------------------------------- /pyptables/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | 4 | from pyptables.tables import Tables, Table 5 | from pyptables.chains import BuiltinChain, UserChain 6 | from pyptables.rules import Rule, Accept, Drop, Jump, Redirect, Return, Log, CustomRule 7 | from pyptables.rules.matches import Match 8 | 9 | 10 | def default_tables(): 11 | """Generate a set of iptables containing all the default tables and chains""" 12 | 13 | return Tables(Table('filter', 14 | BuiltinChain('INPUT', 'ACCEPT'), 15 | BuiltinChain('FORWARD', 'ACCEPT'), 16 | BuiltinChain('OUTPUT', 'ACCEPT'), 17 | ), 18 | Table('nat', 19 | BuiltinChain('PREROUTING', 'ACCEPT'), 20 | BuiltinChain('OUTPUT', 'ACCEPT'), 21 | BuiltinChain('POSTROUTING', 'ACCEPT'), 22 | ), 23 | Table('mangle', 24 | BuiltinChain('PREROUTING', 'ACCEPT'), 25 | BuiltinChain('INPUT', 'ACCEPT'), 26 | BuiltinChain('FORWARD', 'ACCEPT'), 27 | BuiltinChain('OUTPUT', 'ACCEPT'), 28 | BuiltinChain('POSTROUTING', 'ACCEPT'), 29 | ), 30 | ) 31 | 32 | 33 | def make_colorizer(code): 34 | def colorizer(string): 35 | return '\x1b[%(code)sm%(string)s\x1b[0m' % {'code': code, 'string': string} 36 | return colorizer 37 | 38 | 39 | def colorize(string): 40 | """Util function to format iptables output for a tty""" 41 | 42 | heading = make_colorizer("1;32") 43 | comment = make_colorizer("32") 44 | bold = make_colorizer("33") 45 | table = make_colorizer("1;36") 46 | chain = make_colorizer("36") 47 | exclamation = make_colorizer("1;31") 48 | commit = make_colorizer("36") 49 | 50 | result = [] 51 | for line in string.split('\n'): 52 | if line.startswith('#'): 53 | if line.endswith('#'): 54 | result.append(heading(line)) 55 | else: 56 | result.append(comment(line)) 57 | elif line.startswith(':'): 58 | result.append(chain(line)) 59 | elif line.startswith('*'): 60 | result.append(table(line)) 61 | elif line == "COMMIT": 62 | result.append(commit(line)) 63 | else: 64 | parts = [] 65 | for part in line.split(): 66 | if part == '!': 67 | parts.append(exclamation(part)) 68 | elif part.startswith('-'): 69 | parts.append(bold(part)) 70 | else: 71 | parts.append(part) 72 | result.append(" ".join(parts)) 73 | return "\n".join(result) 74 | 75 | 76 | strip_ANSI_escape_sequences_sub = re.compile(r""" 77 | \x1b # literal ESC 78 | \[ # literal [ 79 | [;\d]* # zero or more digits or semicolons 80 | [A-Za-z] # a letter 81 | """, re.VERBOSE).sub 82 | 83 | 84 | def uncolorize(string): 85 | return strip_ANSI_escape_sequences_sub("", string) 86 | 87 | 88 | def add_line_numbers(string, start=1): 89 | """Util function to add line numbers to a string""" 90 | 91 | lines = string.split('\n') 92 | return "\n".join([("%0" + str(len(str(len(lines)))) + "s | %s") % i for i in enumerate(lines, start)]) 93 | 94 | 95 | def restore(tables): 96 | process = subprocess.Popen( 97 | ["iptables-restore"], 98 | stdin=subprocess.PIPE, 99 | stdout=subprocess.PIPE, 100 | stderr=subprocess.PIPE, 101 | ) 102 | if hasattr(tables, 'to_iptables'): 103 | tables = tables.to_iptables() 104 | tables = tables.encode('utf-8') 105 | return process.communicate(tables) 106 | -------------------------------------------------------------------------------- /pyptables/__main__.py: -------------------------------------------------------------------------------- 1 | from pyptables import default_tables, CustomRule, Jump, UserChain, colorize, add_line_numbers 2 | 3 | import sys 4 | 5 | if '--colorize' in sys.argv: 6 | output = colorize(sys.stdin.read()) 7 | else: 8 | tables = default_tables() 9 | 10 | tables['filter']['INPUT'].append(CustomRule('a rule')) 11 | 12 | my_chain = tables['mangle'].append(UserChain( 13 | 'my_chain', 14 | 'A chain to rule all chains', 15 | [CustomRule('init rule')], 16 | )) 17 | tables['mangle']['POSTROUTING'].append(Jump( 18 | chain=my_chain, 19 | proto__not='tcp', 20 | comment='Jump to "%s" for all non-tcp packets' % my_chain.name, 21 | )) 22 | my_chain.append(CustomRule('another rule')) 23 | my_chain.append(CustomRule('a custom rule', comment='A comment "do stuff"')) 24 | 25 | if sys.stdout.isatty() or '--color' in sys.argv: 26 | output = colorize(tables.to_iptables()) 27 | else: 28 | output = tables.to_iptables() 29 | 30 | if '--line-numbers' in sys.argv: 31 | output = add_line_numbers(output) 32 | 33 | print(output) 34 | -------------------------------------------------------------------------------- /pyptables/base.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | class DebugObject(object): 5 | """Base class for most iptables classes. 6 | Allows objects to determine the source line they were created from, 7 | which is used to insert debugging information into the generated output 8 | """ 9 | def __init__(self, *args, **kwargs): 10 | super(DebugObject, self).__init__(*args, **kwargs) 11 | frame = inspect.currentframe().f_back 12 | while frame: 13 | info = inspect.getframeinfo(frame) 14 | if not info[2].startswith('__'): 15 | break 16 | frame = frame.f_back 17 | self.filename, self.lineno, self.function, __, __ = info 18 | 19 | def debug_info(self): 20 | """Returns a string of debug info about the creation of this object""" 21 | return "%s:%s %s" % (self.filename, self.lineno, self.function) 22 | -------------------------------------------------------------------------------- /pyptables/chains.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import namedtuple 3 | 4 | from pyptables.base import DebugObject 5 | 6 | 7 | class AbstractChain(DebugObject, list): 8 | """Represents an iptables Chain. Holds a number of Rule objects in a list-like fashion""" 9 | Result = namedtuple('ChainResult', 'header_content rules') 10 | 11 | def __init__(self, name, comment=None, rules=()): 12 | super(AbstractChain, self).__init__(rules) 13 | self.comment = comment 14 | self.name = name 15 | 16 | def to_iptables(self): 17 | """Returns this chain in a format compatible with iptables-restore""" 18 | try: 19 | prefix = '-A %s' % (self.name,) 20 | if self: 21 | rule_output = [rule.to_iptables(prefix=prefix) for rule in self] 22 | rule_output = "\n".join(rule_output) 23 | else: 24 | rule_output = '# No rules' 25 | return AbstractChain.Result(header_content=self._chain_definition(), 26 | rules="%(comment)s\n%(rules)s" % { 27 | 'comment': self._comment(), 28 | 'rules': rule_output, 29 | }, 30 | ) 31 | except Exception as e: # pragma: no cover 32 | e.iptables_path = getattr(e, 'iptables_path', []) 33 | e.iptables_path.insert(0, self.name) 34 | raise 35 | 36 | def _chain_definition(self): 37 | """Return iptables-restore formatted instruction to create 38 | the chain (note: rules are added separately) 39 | """ 40 | 41 | raise NotImplemented('Subclasses must define this method') # pragma: no cover 42 | 43 | def _comment(self): 44 | comment = '# %(type)s "%(name)s" (%(debug)s)"' % { 45 | 'type': self._type_name(), 46 | 'name': self.name, 47 | 'debug': self.debug_info(), 48 | } 49 | if self.comment: 50 | comment = "%s\n# %s" % (comment, self.comment) 51 | return comment 52 | 53 | def _type_name(self): 54 | return " ".join(re.findall(r'[A-Z][^A-Z]*', self.__class__.__name__)) 55 | 56 | def __repr__(self): 57 | truncated = [str(i) for i in self[:3]] + (['...'] if len(self) > 3 else []) 58 | return "<%s: %s - [%s]>" % (self.__class__.__name__, self.name, ", ".join(truncated)) 59 | 60 | 61 | class UserChain(AbstractChain): 62 | def __init__(self, *args, **kwargs): 63 | super(UserChain, self).__init__(*args, **kwargs) 64 | 65 | def _chain_definition(self): 66 | return ':%(name)s - [0:0]' % {'name': self.name} 67 | 68 | 69 | class BuiltinChain(AbstractChain): 70 | """Represents a built-in iptables chain 71 | Built-in chains can have a default policy""" 72 | 73 | def __init__(self, name, policy, *args, **kwargs): 74 | super(BuiltinChain, self).__init__(name, *args, **kwargs) 75 | self.policy = policy 76 | 77 | def _chain_definition(self): 78 | return ':%(name)s %(policy)s [0:0]' % {'name': self.name, 'policy': self.policy} 79 | -------------------------------------------------------------------------------- /pyptables/rules/__init__.py: -------------------------------------------------------------------------------- 1 | """This package contains classes to generate 2 | rules for iptables. 3 | """ 4 | 5 | from pyptables.rules.base import AbstractRule, CustomRule, Rule, CompositeRule 6 | 7 | from pyptables.chains import AbstractChain as _AbstractChain 8 | 9 | 10 | class Jump(Rule): 11 | """A iptables Rule object that jumps to the specified chain""" 12 | 13 | def __init__(self, chain, comment=None, *args, **kwargs): 14 | """Creates a Jump rule. 15 | chain - a UserChain object or a literal chain name 16 | """ 17 | if isinstance(chain, _AbstractChain): 18 | name = chain.name 19 | if comment is None: 20 | comment = chain.comment 21 | else: 22 | name = chain 23 | 24 | super(Jump, self).__init__(jump=name, comment=comment, *args, **kwargs) 25 | 26 | 27 | Accept = Rule(jump=Rule.ACCEPT) 28 | Drop = Rule(jump=Rule.DROP) 29 | Reject = Rule(jump=Rule.REJECT) 30 | Return = Rule(jump=Rule.RETURN) 31 | Redirect = Rule(jump=Rule.REDIRECT) 32 | Log = Rule(jump=Rule.LOG) 33 | -------------------------------------------------------------------------------- /pyptables/rules/arguments.py: -------------------------------------------------------------------------------- 1 | """This modules contains classes related to rule arguments""" 2 | 3 | from collections import namedtuple 4 | 5 | 6 | class UnboundArgument(object): 7 | """This class represents an argument that the system is 8 | aware of, and can therefore provide additional APIs for. 9 | """ 10 | 11 | ParseResult = namedtuple('ParseResult', 'name inverse') 12 | 13 | def __init__(self, short_name, long_name, type=str, invertable=False): 14 | """Creates an UnboundArgument. 15 | 16 | short_name - short name 17 | long_name - long name 18 | type - argument type (str (default), bool, etc.) 19 | invertable - if true, argument can used with __not prefix 20 | to invert the match (default: False) 21 | """ 22 | super(UnboundArgument, self).__init__() 23 | self.short_name = short_name 24 | self.long_name = long_name 25 | self.type = type 26 | self.invertable = invertable 27 | 28 | def matches(self, name): 29 | """Tests if the passed name matches this argument""" 30 | return bool(self._parse_name(name)) 31 | 32 | def _parse_name(self, name): 33 | """Parses the passed argument name and return a tuple(name, invertable) 34 | containing the name and if it is invertable or not. 35 | """ 36 | parts = name.split('__') 37 | if parts[0] != self.short_name and parts[0] != self.long_name: 38 | return False 39 | 40 | inverse = False 41 | for part in parts[1:]: 42 | if part != 'not': 43 | raise ValueError("Only 'not' is supported") 44 | if not self.invertable: 45 | raise ValueError('This argument is not invertable') 46 | inverse = not inverse 47 | return UnboundArgument.ParseResult(name=parts[0], inverse=inverse) 48 | 49 | def bind(self, name, value): 50 | """Returns a BoundArgument, binding this argument to a value""" 51 | parse_result = self._parse_name(name) 52 | if not parse_result: 53 | raise ValueError('name does not match') 54 | return BoundArgument(self, value, parse_result.inverse) 55 | 56 | def help(self): 57 | return "%s, %s (type: %s%s)" % (self.short_name, 58 | self.long_name, 59 | self.type.__name__, 60 | ', invertable' if self.invertable else '', 61 | ) 62 | 63 | 64 | class Argument(object): 65 | """Represents a iptables Rule argument/value pair (abstract)""" 66 | 67 | def __init__(self, value): 68 | """Create an Argument with the specified value 69 | 70 | Note: arguments may have many names, and these are 71 | specified by the subclasses of this class 72 | """ 73 | self.value = self._parse_value(value) 74 | 75 | def _parse_value(self, values): 76 | if not isinstance(values, list): 77 | values = [values] 78 | result = [] 79 | for value in values: 80 | value = value.replace('"', '\\"') 81 | if ' ' in value: 82 | value = '"%s"' % value 83 | result.append(value) 84 | return " ".join(result) 85 | 86 | def get_name(self): 87 | """Returns the preferred name for this argument""" 88 | raise NotImplemented('Subclasses must implement') # pragma: no cover 89 | 90 | def has_name(self, name): 91 | """Returns True if this argument is known by the specified name""" 92 | raise NotImplemented('Subclasses must implement') # pragma: no cover 93 | 94 | def get_argument(self): 95 | """Renders the argument name with prefixed "-" or "--", as appropriate""" 96 | name = self.get_name().replace('_', '-') 97 | prefix = "-" if len(name) == 1 else "--" 98 | return "%s%s" % (prefix, name) 99 | 100 | def __repr__(self): 101 | return "<%s: %s=%s>" % (self.__class__.__name__, self.get_name(), self.value) 102 | 103 | 104 | class BoundArgument(Argument): 105 | """Represents an known argument (UnboundArgument) bound to value""" 106 | 107 | def __init__(self, argument, value, inverse): 108 | """Creates a BoundArgument 109 | 110 | argument - the UnboundArgument 111 | value - the value 112 | inverse - the rule should pass for values that don't match 113 | """ 114 | self.argument = argument 115 | super(BoundArgument, self).__init__(value) 116 | self.inverse = inverse 117 | 118 | def _parse_value(self, value): 119 | if not isinstance(value, self.argument.type): 120 | raise ValueError('Argument must be of type %s' % self.argument.type) 121 | return super(BoundArgument, self)._parse_value(value) 122 | 123 | def get_name(self): 124 | """Returns the preferred name for this argument""" 125 | if self.argument.short_name: 126 | return self.argument.short_name 127 | return self.argument.long_name 128 | 129 | def has_name(self, name): 130 | """Returns True if this argument is known by the specified name""" 131 | return name == self.argument.short_name or name == self.argument.long_name 132 | 133 | def to_iptables(self): 134 | """Return argument in iptables format, suitable for use in an iptables format rule""" 135 | if self.inverse: 136 | return "! %s %s" % (self.get_argument(), self.value) 137 | return "%s %s" % (self.get_argument(), self.value) 138 | 139 | 140 | class CustomArgument(Argument): 141 | """Represents an iptables argument that the system has no 142 | explicit knowledge of. 143 | """ 144 | 145 | def __init__(self, name, value): 146 | """Create a CustomArgument""" 147 | super(CustomArgument, self).__init__(value) 148 | parts = name.split('__') 149 | if len(parts) == 1: 150 | self.inverse = False 151 | self.name = name 152 | elif len(parts) == 2: 153 | if parts[1] == 'not': 154 | self.inverse = True 155 | self.name = parts[0] 156 | else: 157 | raise ValueError("Only 'not' is supported") 158 | else: 159 | raise ValueError("badly formatted argument name") 160 | 161 | def _parse_value(self, value): 162 | if value is None: 163 | return None 164 | return super(CustomArgument, self)._parse_value(value) 165 | 166 | def get_name(self): 167 | """Returns the preferred name for this argument""" 168 | return self.name 169 | 170 | def has_name(self, name): 171 | """Returns True if this argument is known by the specified name""" 172 | return name == self.name 173 | 174 | def to_iptables(self): 175 | """Return argument in iptables format, suitable for use in an iptables format rule""" 176 | if self.value is None: 177 | result = "%s" % self.get_argument() 178 | else: 179 | result = "%s %s" % (self.get_argument(), self.value) 180 | if self.inverse: 181 | return "! %s" % result 182 | return result 183 | 184 | 185 | class ArgumentList(object): 186 | """Represents a list of iptables Arguments 187 | 188 | Can be iterated: 189 | for arg in arglist: 190 | pass 191 | 192 | Can be indexed by argument name, e.g.: 193 | p = arglist['proto'] 194 | 195 | Can be tested for containment, e.g.: 196 | if 'p' in arglist: 197 | pass 198 | """ 199 | def __init__(self, known_args=(), args=(), **kwargs): 200 | """Creates an ArgumentList 201 | 202 | known_args - list of UnboundArguments known to this ArgumentList 203 | mostly used by subclasses 204 | kwargs - any iptables arguments, known or unknown 205 | args - other ArgumentList objects to add to this ArgumentList 206 | """ 207 | super(ArgumentList, self).__init__() 208 | self.known_args = list(known_args) 209 | self.args = args 210 | self.kwargs = kwargs 211 | 212 | def __call__(self, args=(), **kwargs): 213 | """Returns a new ArgumentList based on this ArgumentList 214 | with the args and kwargs specified added to it 215 | """ 216 | args, kwargs = self._update_args(args, kwargs) 217 | return ArgumentList(known_args=self.known_args, args=args, **kwargs) 218 | 219 | def _update_args(self, args, kwargs): 220 | args, kwargs = list(args), dict(kwargs) # don't modify passed data 221 | for arglist in args: 222 | arglist.known_args.extend(self.known_args) 223 | args.extend(self.args) 224 | kwargs.update(self.kwargs) 225 | return args, kwargs 226 | 227 | def __iter__(self): 228 | kwargs = dict(self.kwargs) # duplicate dictionary, as it is modified below 229 | for argument in self.known_args: 230 | for key in kwargs: 231 | if argument.matches(key): 232 | value = kwargs.pop(key) 233 | yield argument.bind(key, value) 234 | break 235 | for name, value in kwargs.items(): 236 | yield CustomArgument(name, value) 237 | for arglist in self.args: 238 | for arg in arglist: 239 | yield arg 240 | 241 | def __getitem__(self, key): 242 | for arg in self: 243 | if arg.has_name(key): 244 | return arg 245 | raise KeyError('argument "%s" not in list' % key) 246 | 247 | def __contains__(self, key): 248 | try: 249 | self[key] 250 | except KeyError: 251 | return False 252 | else: 253 | return True 254 | 255 | def to_iptables(self): 256 | """Return arguments in iptables format, suitable for use in an iptables format rule""" 257 | return " ".join([arg.to_iptables() for arg in self]) 258 | 259 | def __str__(self): 260 | return str(self.to_iptables()) 261 | 262 | def __repr__(self): 263 | return "<%s: %s>" % (self.__class__.__name__, self.to_iptables()) 264 | -------------------------------------------------------------------------------- /pyptables/rules/base.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from pyptables.base import DebugObject 4 | 5 | from pyptables.rules.arguments import UnboundArgument, ArgumentList 6 | from pyptables.rules.matches import Match 7 | 8 | 9 | class AbstractRule(DebugObject): 10 | """Represents an iptables rule""" 11 | 12 | def __init__(self, comment=None): 13 | super(AbstractRule, self).__init__() 14 | self.comment = comment 15 | 16 | def to_iptables(self, prefix=''): 17 | """Return rule in iptables format, suitable for use with iptables-restore""" 18 | try: 19 | return '%(header)s\n%(rules)s' % { 20 | 'header': self._header(), 21 | 'rules': self._rule_definition(prefix), 22 | } 23 | except Exception as e: # pragma: no cover 24 | e.iptables_path = getattr(e, 'iptables_path', []) 25 | e.iptables_path.insert(0, "Rule:\n created: %s\n comment: %s" % (self.debug_info(), self.comment)) 26 | raise 27 | 28 | def _header(self): 29 | return '# Rule: %(comment)s(%(debug)s)' % { 30 | 'comment': self.comment + ' ' if self.comment else '', 31 | 'debug': self.debug_info(), 32 | } 33 | 34 | def _rule_definition(self, prefix): 35 | if prefix: 36 | prefix += ' ' 37 | return "\n".join(['%s%s' % (prefix, rule) for rule in self.rule_definitions()]) 38 | 39 | def rule_definitions(self): 40 | """Return a list of individual iptables commands that implement this rule""" 41 | raise NotImplementedError() # pragma: no cover 42 | 43 | def __repr__(self): 44 | return "<%s: %s>" % (self.__class__.__name__, self.rule_definitions()) 45 | 46 | 47 | class CustomRule(AbstractRule): 48 | """An iptables rule with its content specified as a plain string""" 49 | def __init__(self, rule, comment=None): 50 | super(CustomRule, self).__init__(comment) 51 | self.rule = rule 52 | 53 | def rule_definitions(self): 54 | """Return a list of individual iptables commands that implement this rule""" 55 | if self.comment: 56 | return ['%s -m comment --comment "%s"' % ( 57 | self.rule, 58 | self.comment.replace('"', '\\"'), 59 | )] 60 | return [self.rule] 61 | 62 | 63 | class Rule(AbstractRule): 64 | """An iptables rule with rich pythonic interface for rule creation""" 65 | 66 | # Handy constants 67 | NONE = 'NONE' 68 | ACCEPT = 'ACCEPT' 69 | DROP = 'DROP' 70 | REJECT = 'REJECT' 71 | RETURN = 'RETURN' 72 | REDIRECT = 'REDIRECT' 73 | LOG = 'LOG' 74 | 75 | TCP = 'tcp' 76 | UDP = 'udp' 77 | ICMP = 'icmp' 78 | IGMP = 'igmp' 79 | 80 | # List of known arguments 81 | _known_args = ( 82 | UnboundArgument('i', 'in_interface', invertable=True), 83 | UnboundArgument('o', 'out_interface', invertable=True), 84 | UnboundArgument('p', 'proto', invertable=True), 85 | UnboundArgument('s', 'source', invertable=True), 86 | UnboundArgument('d', 'destination', invertable=True), 87 | UnboundArgument('f', 'fragment'), 88 | UnboundArgument('j', 'jump'), 89 | UnboundArgument('g', 'goto'), 90 | ) 91 | 92 | def __init__(self, comment=None, args=(), **kwargs): 93 | """Creates a Rule. 94 | 95 | comment - rule comment 96 | kwargs - any iptables arguments, known or unknown 97 | args - ArgumentList objects to add to this rule 98 | 99 | Some arguments are invertable by appending __not to the 100 | argument name (see Known Arguments below). 101 | 102 | Usage: 103 | Rule(jump='DROP', i='eth0', destination__not='192.168.23.0/24') 104 | """ 105 | super(Rule, self).__init__(comment) 106 | self.arguments = ArgumentList(known_args=self._known_args, args=args, **kwargs) 107 | __init__.__doc__ = "%s\nKnown arguments:\n%s" % (__init__.__doc__, 108 | "\n".join(arg.help() for arg in _known_args), 109 | ) 110 | 111 | def __call__(self, comment=None, args=(), **kwargs): 112 | """Returns a new rule based on this rule with the args and kwargs specified added to it""" 113 | rule = Rule() 114 | rule.comment = comment or self.comment 115 | rule.arguments = self.arguments(args=args, **kwargs) 116 | return rule 117 | 118 | def rule_definitions(self): 119 | """Return a list of individual iptables commands that implement this rule""" 120 | arguments = list(self.arguments) 121 | if self.comment: 122 | arguments.append(Match('comment', comment=self.comment)) 123 | return [" ".join([arg.to_iptables() for arg in arguments])] 124 | 125 | 126 | class CompositeRule(AbstractRule): 127 | """An iptables rule combining multiple other iptables rules (AbstractRule derivatives)""" 128 | def __init__(self, rules, comment=None): 129 | super(CompositeRule, self).__init__(comment) 130 | self._rules = rules 131 | 132 | def rule_definitions(self): 133 | """Return a list of individual iptables commands that implement this rule""" 134 | return itertools.chain(*(rule.rule_definitions() for rule in self._rules)) 135 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/__init__.py: -------------------------------------------------------------------------------- 1 | """This package contains classes to generate "forwarding" 2 | rules for iptables. 3 | """ 4 | 5 | from pyptables.rules.forwarding.base import ForwardingRule 6 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/base.py: -------------------------------------------------------------------------------- 1 | from pyptables.rules import Accept, Drop, AbstractRule, Rule, Reject 2 | 3 | 4 | class ForwardingRule(AbstractRule): 5 | """This class represents an iptables rule for forwarding 6 | packets from one location to another. 7 | """ 8 | 9 | def __init__(self, policy, sources, destinations, channels=(), log=False, 10 | log_id=None, log_cls=None, comment=None, args=None): 11 | """Creates a ForwardingRule 12 | 13 | policy - the action to take (ACCEPT, DROP, REJECT, etc.) on matching the rule 14 | sources - the source location(s) to match 15 | destinations - the destination location(s) to match 16 | channels - the channel(s) to match 17 | log - boolean indicated if "hits" on this rule should be logged 18 | comment - a comment for the rule 19 | args - list of ArgumentLists of additional arguments to match 20 | """ 21 | super(ForwardingRule, self).__init__(comment) 22 | self.policy = policy 23 | self.sources = list(sources) 24 | self.destinations = list(destinations) 25 | self.channels = list(channels) 26 | self.log = log 27 | self.log_id = log_id 28 | self.log_cls = log_cls 29 | self.args = args 30 | 31 | def rule_definitions(self): 32 | """Generate the iptables rules for this rule""" 33 | result = [] 34 | for rule in self._rules(): 35 | result.extend(rule.rule_definitions()) 36 | return result 37 | 38 | def _base_rules(self): 39 | rules = [] 40 | if self.log: 41 | rules.append(self.log_cls(prefix='FWD %s %s' % (self.log_id, self.policy.upper()[0]), 42 | comment=self.comment, 43 | )) 44 | 45 | if self.policy.upper() == Rule.ACCEPT: 46 | rules.append(Accept(comment=self.comment)) 47 | elif self.policy.upper() == Rule.DROP: 48 | rules.append(Drop(comment=self.comment)) 49 | elif self.policy.upper() == Rule.REJECT: 50 | rules.append(Reject(comment=self.comment)) 51 | elif self.policy.upper() == Rule.NONE: 52 | pass 53 | else: 54 | raise ValueError('policy must be either %s, %s, %s or %s' % ( 55 | Rule.ACCEPT, 56 | Rule.DROP, 57 | Rule.REJECT, 58 | Rule.NONE, 59 | )) 60 | 61 | return rules 62 | 63 | def _rules(self): 64 | rules = self._base_rules() 65 | rules = self._add_routes(rules) 66 | rules = self._add_channels(rules) 67 | rules = self._add_args(rules) 68 | return rules 69 | 70 | def _add_routes(self, rules): 71 | if not (self.sources or self.destinations): 72 | return rules 73 | 74 | result = [] 75 | for rule in rules: 76 | if not self.sources: 77 | for destination in self.destinations: 78 | result.append(rule( 79 | args=(destination.as_output(),), 80 | comment="%s: route any -> %s" % ( 81 | rule.comment, 82 | destination, 83 | ), 84 | )) 85 | elif not self.destinations: 86 | for source in self.sources: 87 | result.append(rule( 88 | args=(source.as_input(),), 89 | comment="%s: route %s -> any" % ( 90 | rule.comment, 91 | source, 92 | ), 93 | )) 94 | else: 95 | for source in self.sources: 96 | for destination in self.destinations: 97 | result.append(rule( 98 | args=(source.as_input(), destination.as_output()), 99 | comment="%s: route %s -> %s" % ( 100 | rule.comment, 101 | source, 102 | destination, 103 | ), 104 | )) 105 | return result 106 | 107 | def _add_channels(self, rules): 108 | if not self.channels: 109 | return rules 110 | result = [] 111 | for rule in rules: 112 | for channel in self.channels: 113 | result.append(rule( 114 | args=[channel], 115 | comment="%s, channel %s" % (rule.comment, channel), 116 | )) 117 | return result 118 | 119 | def _add_args(self, rules): 120 | if not self.args: 121 | return rules 122 | result = [] 123 | for rule in rules: 124 | result.append(rule( 125 | args=self.args, 126 | comment="%s, plus %s" % ( 127 | rule.comment, 128 | ", ".join(map(str, self.args)), 129 | ), 130 | )) 131 | return result 132 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/channels.py: -------------------------------------------------------------------------------- 1 | """This module contains the Channel classes. 2 | Channels represent a specification of a conduit in terms of 3 | a L2 protocol (or above). 4 | """ 5 | 6 | from pyptables.rules.arguments import ArgumentList, UnboundArgument 7 | from pyptables.rules.matches import Match 8 | 9 | 10 | class Channel(ArgumentList): 11 | """Channels represent a L3 network protocol""" 12 | 13 | def __init__(self, known_args=(), **kwargs): 14 | """Creates a Channel. 15 | 16 | p, proto - L3 protocol name (tcp, udp, etc.) 17 | kwargs - Additional iptables arguments (see ArgumentList) 18 | """ 19 | known_args += (UnboundArgument('p', 'proto'),) 20 | super(Channel, self).__init__(known_args=known_args, **kwargs) 21 | 22 | def __str__(self): 23 | return "%s" % self['p'].value 24 | 25 | 26 | class StatefulChannel(Channel): 27 | """A Channel capable of tracking connection state""" 28 | 29 | def __init__(self, states='', args=None, **kwargs): 30 | """Creates a StatefulChannel 31 | 32 | states - The states to match 33 | kwargs - Additional iptables arguments (see ArgumentList) 34 | """ 35 | args = args or [] 36 | if states: 37 | args.append(Match('conntrack', ctstate=states)) 38 | super(StatefulChannel, self).__init__(args=args, **kwargs) 39 | 40 | def __str__(self): 41 | base_str = super(StatefulChannel, self).__str__() 42 | if 'ctstate' in self: 43 | return "%s, %s" % (base_str, self['ctstate'].value) 44 | return base_str 45 | 46 | 47 | class PortChannel(StatefulChannel): 48 | """A StatefulChannel with port information. May only be used with "proto" that supports ports.""" 49 | 50 | def __init__(self, sports='', dports='', args=None, **kwargs): 51 | """Creates a PortChannel 52 | 53 | sports - source ports to match 54 | dports - destination ports to match 55 | """ 56 | args = args or [] 57 | if sports or dports: 58 | multiport_args = {} 59 | if dports: 60 | dports = dports.replace('-', ':').replace(' ', '') 61 | multiport_args['dports'] = dports 62 | if sports: 63 | sports = sports.replace('-', ':').replace(' ', '') 64 | multiport_args['sports'] = sports 65 | args.append(Match('multiport', **multiport_args)) 66 | super(PortChannel, self).__init__(args=args, **kwargs) 67 | 68 | def __str__(self): 69 | sports = self['sports'].value if 'sports' in self else 'any' 70 | dports = self['dports'].value if 'dports' in self else 'any' 71 | return "%s, ports %s -> %s" % (super(PortChannel, self).__str__(), sports, dports) 72 | 73 | 74 | class TCPChannel(PortChannel): 75 | """A TCPChannel represents a TCP port and/or connection state specification""" 76 | 77 | def __init__(self, **kwargs): 78 | super(TCPChannel, self).__init__(proto='tcp', **kwargs) 79 | 80 | 81 | class UDPChannel(PortChannel): 82 | """A UDPChannel represents a UDP port and/or connection state specification""" 83 | 84 | def __init__(self, **kwargs): 85 | super(UDPChannel, self).__init__(proto='udp', **kwargs) 86 | 87 | 88 | class ICMPChannel(StatefulChannel): 89 | """A ICMPChannel represents a ICMP port and/or connection state specification""" 90 | 91 | def __init__(self, icmp_type='', args=None, **kwargs): 92 | if icmp_type: 93 | super(ICMPChannel, self).__init__(proto='icmp', args=args, icmp_type=icmp_type, **kwargs) 94 | else: 95 | super(ICMPChannel, self).__init__(proto='icmp', args=args, **kwargs) 96 | 97 | def __str__(self): 98 | icmp_type = self['icmp_type'].value if 'icmp_type' in self else 'any' 99 | return "%s, type %s" % (super(ICMPChannel, self).__str__(), icmp_type) 100 | 101 | 102 | __all__ = [Channel, StatefulChannel, PortChannel, TCPChannel, UDPChannel, ICMPChannel] 103 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/hosts.py: -------------------------------------------------------------------------------- 1 | """This module contains the Hosts class. 2 | Hosts represent a collection of network addresses 3 | """ 4 | 5 | from pyptables.rules.arguments import ArgumentList 6 | from pyptables.rules.matches import Match 7 | from pyptables.base import DebugObject 8 | 9 | 10 | class Hosts(DebugObject): 11 | """Represents a collection of network addresses""" 12 | 13 | def as_input(self): 14 | """Return iptables ArgumentLists for this group of 15 | hosts for matching against packet sources 16 | """ 17 | raise NotImplementedError() # pragma: no cover 18 | 19 | def as_output(self): 20 | """Return iptables ArgumentLists for this group of 21 | hosts for matching against packet destinations 22 | """ 23 | raise NotImplementedError() # pragma: no cover 24 | 25 | @staticmethod 26 | def from_ip_list(string): 27 | """Generate a list of Hosts objects from a string. 28 | 29 | The string may contain comma separated ips, subnets 30 | (in CIDR notation), or ip ranges (from-to). 31 | """ 32 | if not string: 33 | return [] # pragma: no cover 34 | parts = string.replace(' ', '').split(',') 35 | singles = [part for part in parts if '-' not in part] 36 | ranges = [part for part in parts if '-' in part] 37 | 38 | result = [] 39 | if singles: 40 | result.append(HostList(singles)) 41 | result.extend([HostRange(r) for r in ranges]) 42 | return result 43 | 44 | 45 | class HostList(Hosts): 46 | def __init__(self, hosts): 47 | super(HostList, self).__init__() 48 | self.hosts = hosts 49 | 50 | def as_input(self): 51 | return ArgumentList(source=",".join(self.hosts)) 52 | 53 | def as_output(self): 54 | return ArgumentList(destination=",".join(self.hosts)) 55 | 56 | def __repr__(self): 57 | return "" % (",".join(self.hosts)) 58 | 59 | def __str__(self): 60 | return ",".join(self.hosts) 61 | 62 | 63 | class HostRange(Hosts): 64 | def __init__(self, range): 65 | super(HostRange, self).__init__() 66 | self.range = range 67 | 68 | def as_input(self): 69 | return Match('iprange', src_range=self.range) 70 | 71 | def as_output(self): 72 | return Match('iprange', src_range=self.range) 73 | 74 | def __repr__(self): 75 | return "" % self.range 76 | 77 | def __str__(self): 78 | return self.range 79 | 80 | 81 | __all__ = [Hosts] 82 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/ipsets.py: -------------------------------------------------------------------------------- 1 | """This module contains the IPSet class. 2 | 3 | IPSet represent an ipset. 4 | """ 5 | 6 | from pyptables.base import DebugObject 7 | from pyptables.rules.matches import Match 8 | 9 | 10 | class IPSet(DebugObject): 11 | """Represents a linux ipset""" 12 | 13 | def __init__(self, name): 14 | """Creates an ipset 15 | 16 | name - ipset name 17 | """ 18 | super(IPSet, self).__init__() 19 | self.name = name 20 | 21 | def as_input(self): 22 | """Return iptables ArgumentLists for this ipset 23 | for matching against packet sources 24 | """ 25 | return Match('set', match_set=[self.name, 'src']) 26 | 27 | def as_output(self): 28 | """Return iptables ArgumentLists for this ipset 29 | for matching against packet destinations 30 | """ 31 | return Match('set', match_set=[self.name, 'dst']) 32 | 33 | def __repr__(self): 34 | return "" % (self.name,) 35 | 36 | def __str__(self): 37 | return self.name 38 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/locations.py: -------------------------------------------------------------------------------- 1 | """This module contains the Location class. 2 | 3 | Locations represent a network location. 4 | """ 5 | 6 | from pyptables.base import DebugObject 7 | from pyptables.rules.arguments import ArgumentList 8 | from pyptables.rules.forwarding.hosts import Hosts 9 | 10 | 11 | class Location(DebugObject): 12 | """Represents a network location""" 13 | 14 | @staticmethod 15 | def from_ip_list(name, zone, ips): 16 | """Generate a list of Location objects from a string of ips 17 | and/or a zone. 18 | 19 | The string may contain comma separated ips, subnets 20 | (in CIDR notation), or ip ranges (from-to). 21 | """ 22 | result = [] 23 | for hosts in Hosts.from_ip_list(ips): 24 | result.append(Location(name, zone, hosts)) 25 | return result 26 | 27 | def __init__(self, name, zone, hosts=None): 28 | """Creates a Location 29 | 30 | name - location name 31 | zone - network 32 | hosts - network addresses 33 | 34 | If no hosts are specified, this location contains the entire zone. 35 | """ 36 | super(Location, self).__init__() 37 | self.name = name 38 | self.zone = zone 39 | self.hosts = hosts 40 | 41 | def as_input(self): 42 | """Return iptables ArgumentLists for this location 43 | for matching against packet sources 44 | """ 45 | if self.hosts: 46 | if self.zone: 47 | return ArgumentList(args=[self.zone.as_input(), self.hosts.as_input()]) 48 | else: 49 | return ArgumentList(args=[self.hosts.as_input()]) 50 | else: 51 | return ArgumentList(args=[self.zone.as_input()]) 52 | 53 | def as_output(self): 54 | """Return iptables ArgumentLists for this location 55 | for matching against packet destinations 56 | """ 57 | if self.hosts: 58 | if self.zone: 59 | return ArgumentList(args=[self.zone.as_output(), self.hosts.as_output()]) 60 | else: 61 | return ArgumentList(args=[self.hosts.as_output()]) 62 | else: 63 | return ArgumentList(args=[self.zone.as_output()]) 64 | 65 | def __repr__(self): 66 | return "" % (self.name, self.zone, self.hosts) 67 | 68 | def __str__(self): 69 | if self.hosts: 70 | return "%s: %s" % (self.zone and self.zone.name or "Anywhere", self.name) 71 | return self.name 72 | -------------------------------------------------------------------------------- /pyptables/rules/forwarding/zones.py: -------------------------------------------------------------------------------- 1 | """This module contains the Zone class. 2 | 3 | Zones represent a network. 4 | """ 5 | 6 | from pyptables.base import DebugObject 7 | from pyptables.rules.arguments import ArgumentList 8 | from pyptables.rules.matches import Match 9 | 10 | 11 | class Zone(DebugObject): 12 | """Represents a network""" 13 | 14 | def __init__(self, name, interface, physdev=None): 15 | """Creates a zone 16 | 17 | name - zone name 18 | interface - the network interface name 19 | """ 20 | super(Zone, self).__init__() 21 | self.name = name 22 | self.interface = interface 23 | self.physdev = physdev 24 | 25 | def as_input(self): 26 | """Return iptables ArgumentLists for this zone 27 | for matching against packet sources 28 | """ 29 | if self.physdev is None: 30 | return ArgumentList(in_interface=self.interface) 31 | return ArgumentList(in_interface=self.interface, args=[Match('physdev', physdev_in=self.physdev)]) 32 | 33 | def as_output(self): 34 | """Return iptables ArgumentLists for this zone 35 | for matching against packet sources 36 | """ 37 | if self.physdev is None: 38 | return ArgumentList(out_interface=self.interface) 39 | return ArgumentList(out_interface=self.interface, args=[Match('physdev', physdev_out=self.physdev)]) 40 | 41 | def __repr__(self): 42 | return "" % (self.name,) 43 | 44 | def __str__(self): 45 | return self.name 46 | -------------------------------------------------------------------------------- /pyptables/rules/input/__init__.py: -------------------------------------------------------------------------------- 1 | """This package contains classes to generate "input" 2 | rules for iptables. 3 | """ 4 | 5 | from pyptables.rules.input.base import InputRule 6 | -------------------------------------------------------------------------------- /pyptables/rules/input/base.py: -------------------------------------------------------------------------------- 1 | from pyptables.rules import Accept, Drop, AbstractRule, Rule, Reject 2 | 3 | 4 | class InputRule(AbstractRule): 5 | """This class represents an iptables rule for forwarding 6 | packets from one location to another. 7 | """ 8 | 9 | def __init__(self, policy, sources=(), channels=(), log=False, log_id=None, log_cls=None, comment=None): 10 | """Creates a ForwardingRule 11 | 12 | policy - the action to take (ACCEPT, DROP, REJECT, etc.) on matching the rule 13 | sources - the source location(s) to match 14 | channels - the channel(s) to match 15 | log - boolean indicated if "hits" on this rule should be logged 16 | comment - a comment for the rule 17 | """ 18 | super(InputRule, self).__init__(comment) 19 | self.policy = policy 20 | self.sources = list(sources) 21 | self.channels = list(channels) 22 | self.log = log 23 | self.log_id = log_id 24 | self.log_cls = log_cls 25 | 26 | def rule_definitions(self): 27 | """Generate the iptables rules for this rule""" 28 | result = [] 29 | for rule in self._rules(): 30 | result.extend(rule.rule_definitions()) 31 | return result 32 | 33 | def _base_rules(self): 34 | rules = [] 35 | if self.log: 36 | rules.append(self.log_cls(prefix='IN %s %s' % (self.log_id, self.policy.upper()[0]), 37 | comment=self.comment, 38 | )) 39 | 40 | if self.policy.upper() == Rule.ACCEPT: 41 | rules.append(Accept(comment=self.comment)) 42 | elif self.policy.upper() == Rule.DROP: 43 | rules.append(Drop(comment=self.comment)) 44 | elif self.policy.upper() == Rule.REJECT: 45 | rules.append(Reject(comment=self.comment)) 46 | elif self.policy.upper() == Rule.NONE: 47 | pass 48 | else: 49 | raise ValueError('policy must be either %s, %s or %s' % (Rule.ACCEPT, Rule.DROP, Rule.REJECT)) 50 | 51 | return rules 52 | 53 | def _rules(self): 54 | rules = self._base_rules() 55 | rules = self._add_routes(rules) 56 | rules = self._add_channels(rules) 57 | return rules 58 | 59 | def _add_routes(self, rules): 60 | if not self.sources: 61 | return rules 62 | 63 | result = [] 64 | for rule in rules: 65 | for source in self.sources: 66 | result.append(rule( 67 | args=(source.as_input(),), 68 | comment="%s: route %s -> any" % ( 69 | rule.comment, 70 | source, 71 | ), 72 | )) 73 | return result 74 | 75 | def _add_channels(self, rules): 76 | if not self.channels: 77 | return rules 78 | result = [] 79 | for rule in rules: 80 | for channel in self.channels: 81 | result.append(rule( 82 | args=[channel], 83 | comment="%s, channel %s" % (rule.comment, channel), 84 | )) 85 | return result 86 | -------------------------------------------------------------------------------- /pyptables/rules/marks.py: -------------------------------------------------------------------------------- 1 | """This module contains utility classes for handling iptables marks""" 2 | 3 | from functools import partial 4 | from random import Random 5 | 6 | from pyptables.rules import Rule 7 | 8 | 9 | class Mark(Rule): 10 | """A Rule that marks matching packets with the specified mark value""" 11 | MARK = 'MARK' 12 | 13 | def __init__(self, mark, *args, **kwargs): 14 | """Creates a Mark rule 15 | 16 | mark - the value to mark matching packets with 17 | """ 18 | super(Mark, self).__init__(jump=Mark.MARK, set_mark=str(mark), *args, **kwargs) 19 | self.mark = mark 20 | 21 | 22 | class Marked(Rule): 23 | """A rule that matches packets with the specified mark""" 24 | 25 | def __init__(self, mark, *args, **kwargs): 26 | """Created a Marked rule 27 | 28 | mark - match this mark value (can be the the Mark 29 | rule used originally mark the packets, or 30 | a literal value 31 | """ 32 | if not isinstance(mark, int): 33 | mark = mark.mark 34 | super(Marked, self).__init__(match='mark', mark=str(mark), *args, **kwargs) 35 | self.mark = mark 36 | 37 | 38 | DropMarked = partial(Marked, jump=Rule.DROP) 39 | AcceptMarked = partial(Marked, jump=Rule.ACCEPT) 40 | 41 | _random_mark_value = partial(Random().randint, 1, 65535) 42 | _marks = [] 43 | 44 | 45 | def random_mark(): 46 | """Generate a Mark rule with a random value""" 47 | mark = _random_mark_value() 48 | while mark in _marks: 49 | mark = _random_mark_value() # pragma: no cover 50 | return Mark(mark=mark) 51 | -------------------------------------------------------------------------------- /pyptables/rules/matches.py: -------------------------------------------------------------------------------- 1 | """This module contains a utility class for handling iptables match extensions""" 2 | 3 | from pyptables.rules.arguments import ArgumentList, UnboundArgument 4 | 5 | 6 | class Match(ArgumentList): 7 | """An iptables ArgumentList for a match extension""" 8 | _known_args = (UnboundArgument('m', 'match'),) 9 | 10 | def __init__(self, name, known_args=(), args=(), **kwargs): 11 | """Creates a Match 12 | 13 | known_args - list of UnboundArguments known to this Match 14 | mostly used by subclasses 15 | name - the name of the match extension 16 | kwargs - any iptables arguments, known or unknown 17 | args - other ArgumentList objects to add to this ArgumentList 18 | """ 19 | known_args = self._known_args + known_args 20 | super(Match, self).__init__(match=name, known_args=known_args, args=args, **kwargs) 21 | -------------------------------------------------------------------------------- /pyptables/tables.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from pyptables.base import DebugObject 4 | 5 | 6 | class Tables(DebugObject, OrderedDict): 7 | """Dictionary like top-level container of iptables, holds a number of Table objects.""" 8 | def __init__(self, *tables): 9 | super(Tables, self).__init__() 10 | for table in tables: 11 | self.append(table) 12 | 13 | def to_iptables(self): 14 | """Returns this list of tables in a format compatible with iptables-restore""" 15 | try: 16 | header = '# Tables generated by PyPTables (%(debug)s)' % {'debug': self.debug_info()} 17 | table_output = [table.to_iptables() for table in self.values()] 18 | table_output = "\n\n".join(table_output) 19 | return "%(header)s\n\n%(tables)s\n" % { 20 | 'header': header, 21 | 'tables': table_output, 22 | } 23 | except Exception as e: # pragma: no cover 24 | e.iptables_path = getattr(e, 'iptables_path', []) 25 | e.iptables_path.insert(0, "Tables") 26 | e.message = "Iptables error at:\n %s\n\nError message: %s" % ( 27 | "\n".join(e.iptables_path).replace('\n', '\n '), 28 | e.message, 29 | ) 30 | raise 31 | 32 | def __setitem__(self, *args, **kwargs): 33 | raise TypeError("Tables object does not support item assignment, use append(table)") 34 | 35 | def append(self, table): 36 | """Append a table to this list of tables""" 37 | super(Tables, self).__setitem__(table.name, table) 38 | return table 39 | 40 | def __repr__(self): 41 | return "" % ", ".join(['' % t.name for t in self.values()]) 42 | 43 | 44 | class Table(DebugObject, OrderedDict): 45 | """Represents an iptables table, holds a number of Chain objects in a dictionary-like fashion""" 46 | def __init__(self, name, *chains): 47 | super(Table, self).__init__() 48 | self.name = name 49 | for chain in chains: 50 | self.append(chain) 51 | 52 | def to_iptables(self): 53 | """Returns this table in a format compatible with iptables-restore""" 54 | try: 55 | header_content = "# %(name)s table (%(debug)s) #" % { 56 | 'name': self.name, 57 | 'debug': self.debug_info(), 58 | } 59 | header = "%(marquee)s\n%(content)s\n%(marquee)s\n*%(name)s" % { 60 | 'content': header_content, 61 | 'marquee': "#"*len(header_content), 62 | 'name': self.name, 63 | } 64 | chain_results = [chain.to_iptables() for chain in self.values()] 65 | 66 | return "%(header)s\n%(chains)s\n\n%(rules)s\n\n%(footer)s" % { 67 | 'header': header, 68 | 'chains': "\n".join([result.header_content for result in chain_results]), 69 | 'rules': "\n\n".join([result.rules for result in chain_results]), 70 | 'footer': 'COMMIT' 71 | } 72 | except Exception as e: # pragma: no cover 73 | e.iptables_path = getattr(e, 'iptables_path', []) 74 | e.iptables_path.insert(0, self.name) 75 | raise 76 | 77 | def __setitem__(self, *args, **kwargs): 78 | raise TypeError("Table object does not support item assignment, use append(chain)") 79 | 80 | def append(self, chain): 81 | """Append a chain to this table""" 82 | super(Table, self).__setitem__(chain.name, chain) 83 | return chain 84 | 85 | def __repr__(self): 86 | return "" % (self.name, list(self.values())) 87 | -------------------------------------------------------------------------------- /pyptables/test/__init__.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import six 4 | from six.moves import zip_longest 5 | import os.path 6 | import unittest 7 | 8 | from io import StringIO 9 | 10 | from pyptables import default_tables, Rule, UserChain, Jump, CustomRule 11 | from pyptables.rules import CompositeRule 12 | from pyptables.rules.arguments import ArgumentList, CustomArgument 13 | from pyptables.rules.marks import Mark, random_mark, Marked 14 | from pyptables.rules.input import InputRule 15 | from pyptables.rules.forwarding import ForwardingRule 16 | from pyptables.rules.forwarding.hosts import HostList, HostRange 17 | from pyptables.rules.forwarding.ipsets import IPSet 18 | from pyptables.rules.forwarding.locations import Location 19 | from pyptables.rules.forwarding.zones import Zone 20 | from pyptables.rules.forwarding.channels import TCPChannel, UDPChannel, ICMPChannel 21 | 22 | 23 | def compare(a, b): 24 | for line_no, lines in enumerate(zip_longest(a, b, fillvalue='')): 25 | lines = [line.strip() for line in lines] 26 | if all(line.startswith('#') for line in lines): 27 | continue 28 | for i, chars in enumerate(zip_longest(*lines)): 29 | if chars[0] != chars[1]: 30 | raise ValueError("line %s doesn't match:\n\t%s\n\t%s\n\t%s^" % ( 31 | line_no, 32 | lines[0], 33 | lines[1], i*'_', 34 | )) # pragma: nocover 35 | return True 36 | 37 | 38 | class MainTest(unittest.TestCase): 39 | def test(self): 40 | with six.assertRaisesRegex(self, ValueError, "Only 'not' is supported"): 41 | Rule(s__invalid=None).to_iptables() 42 | with six.assertRaisesRegex(self, ValueError, "This argument is not invertable"): 43 | Rule(f__not=None).to_iptables() 44 | with six.assertRaisesRegex(self, ValueError, "Only 'not' is supported"): 45 | Rule(custom__invalid=None).to_iptables() 46 | with six.assertRaisesRegex(self, ValueError, "badly formatted argument name"): 47 | Rule(custom__not__invalid=None).to_iptables() 48 | Rule(custom__not=None).to_iptables() 49 | arg_list = ArgumentList(custom='1') 50 | self.assertIsInstance(arg_list['custom'], CustomArgument) 51 | with self.assertRaises(KeyError): 52 | _ = arg_list['missing'] 53 | arg_list = arg_list(another=None, args=(ArgumentList(custom='2'),)) 54 | self.assertTrue('custom' in arg_list) 55 | self.assertFalse('missing' in arg_list) 56 | self.assertIsInstance(arg_list['custom'], CustomArgument) 57 | self.assertIsInstance(arg_list['another'], CustomArgument) 58 | self.assertEqual(arg_list.to_iptables(), "--another --custom 1 --custom 2") 59 | repr(Rule(j='DROP').arguments) 60 | tables = default_tables() 61 | chain = UserChain('test_chain', comment='A user chain') 62 | repr(chain) 63 | repr(Rule()) 64 | Accept = Rule(j='ACCEPT') 65 | chain.append(Rule(i='eth0', s='1.1.2.1', d__not='1.1.1.2', jump='DROP', comment='A Rule')) 66 | 67 | with self.assertRaises(TypeError): 68 | tables['filter'] = None 69 | print(tables) 70 | 71 | with self.assertRaises(TypeError): 72 | tables['filter']['INPUT'] = None 73 | print(tables['filter']) 74 | 75 | tables['filter'].append(chain) 76 | tables['filter']['INPUT'].append(Jump(chain)) 77 | tables['filter']['INPUT'].append(Jump('string_chain')) 78 | tables['filter']['OUTPUT'].append(CustomRule('a random string')) 79 | tables['filter']['OUTPUT'].append(CustomRule('a random string', comment='this is a custom rule with a comment')) 80 | tables['filter']['OUTPUT'].append(CustomRule('a random string', comment='this is a custom rule with a comment')) 81 | tables['filter']['OUTPUT'].append(CompositeRule([Accept(s='1.1.1.1'), Rule(j='DROP')])) 82 | tables['mangle']['OUTPUT'].append(Mark(123)) 83 | random = random_mark() 84 | 85 | Log = Rule(j='LOG') 86 | simple_zone = Zone('A zone', 'eth0') 87 | repr(simple_zone) 88 | simple_location = Location('A location3', Zone('A zone', 'br0', physdev='eth0')) 89 | repr(simple_location) 90 | ip_set = IPSet('a_set') 91 | repr(ip_set) 92 | list_location = Location.from_ip_list('A location', None, '1.1.1.1,2.2.2.2') 93 | range_location = Location.from_ip_list('A location2', simple_zone, '3.1.1.1-3.2.2.2') 94 | tables['mangle']['OUTPUT'].append(Marked(random)) 95 | tables['mangle']['INPUT'].append(InputRule(policy='DROP', 96 | channels=[], 97 | sources=itertools.chain(list_location, 98 | range_location, 99 | [simple_location, ip_set], 100 | ), 101 | log=True, 102 | log_cls=Log, 103 | )) 104 | tcp = TCPChannel(sports='1', dports='2') 105 | udp = UDPChannel(states='ESTABLISHED') 106 | icmp1 = ICMPChannel(icmp_type='1') 107 | icmp2 = ICMPChannel() 108 | host_list = HostList(['1.1.1.1', '2.2.2.2']) 109 | repr(host_list) 110 | str(host_list) 111 | host_range = HostRange('1.1.1.1-2.2.2.2') 112 | repr(host_range) 113 | str(host_range) 114 | tables['mangle']['INPUT'].append(InputRule('ACCEPT', 115 | channels=[tcp, udp, icmp1, icmp2], 116 | )) 117 | tables['mangle']['INPUT'].append(InputRule('REJECT')) 118 | tables['mangle']['INPUT'].append(InputRule('NONE')) 119 | tables['filter']['FORWARD'].append(ForwardingRule(policy='DROP', 120 | sources=[], 121 | destinations=[simple_location, ip_set], 122 | )) 123 | tables['filter']['FORWARD'].append(ForwardingRule(policy='ACCEPT', 124 | sources=list_location, 125 | destinations=[], 126 | )) 127 | tables['filter']['FORWARD'].append(ForwardingRule(policy='REJECT', 128 | sources=range_location, 129 | destinations=list_location, 130 | )) 131 | tables['filter']['FORWARD'].append(ForwardingRule(policy='REJECT', 132 | sources=[], 133 | destinations=range_location, 134 | )) 135 | tables['filter']['FORWARD'].append(ForwardingRule(policy='NONE', 136 | sources=[], 137 | destinations=[], 138 | channels=[tcp, udp, icmp1, icmp2], 139 | args=[host_list.as_input(), host_range.as_input()], 140 | log=True, 141 | log_cls=Log, 142 | )) 143 | with self.assertRaises(ValueError): 144 | InputRule('BAD').to_iptables() 145 | with self.assertRaises(ValueError): 146 | ForwardingRule(policy='BAD', sources=[], destinations=[]).to_iptables() 147 | with six.assertRaisesRegex(self, ValueError, "Argument must be of type.*"): 148 | Rule(p=1).to_iptables() 149 | result = tables.to_iptables() 150 | fixture_file = os.path.join(os.path.dirname(__file__), 'test.dat') 151 | with open(fixture_file, 'w') as fixture: 152 | fixture.write(result) 153 | with open(fixture_file) as fixture: 154 | try: 155 | compare(fixture, StringIO(six.u(result))) 156 | except ValueError as e: # pragma: nocover 157 | self.fail(str(e)) 158 | -------------------------------------------------------------------------------- /pyptables/test/test.dat: -------------------------------------------------------------------------------- 1 | # Tables generated by PyPTables (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:28 default_tables) 2 | 3 | ############################################################################################## 4 | # filter table (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:16 default_tables) # 5 | ############################################################################################## 6 | *filter 7 | :INPUT ACCEPT [0:0] 8 | :FORWARD ACCEPT [0:0] 9 | :OUTPUT ACCEPT [0:0] 10 | :test_chain - [0:0] 11 | 12 | # Builtin Chain "INPUT" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:14 default_tables)" 13 | # Rule: A user chain (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:76 test) 14 | -A INPUT -j test_chain -m comment --comment "A user chain" 15 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:77 test) 16 | -A INPUT -j string_chain 17 | 18 | # Builtin Chain "FORWARD" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:15 default_tables)" 19 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:121 test) 20 | -A FORWARD -j DROP --out-interface br0 -m physdev --physdev-out eth0 -m comment --comment "None: route any -> A location3" 21 | -A FORWARD -j DROP -m set --match-set a_set dst -m comment --comment "None: route any -> a_set" 22 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:125 test) 23 | -A FORWARD -j ACCEPT --source 1.1.1.1,2.2.2.2 -m comment --comment "None: route Anywhere: A location -> any" 24 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:129 test) 25 | -A FORWARD -j REJECT --in-interface eth0 -m iprange --src-range 3.1.1.1-3.2.2.2 --destination 1.1.1.1,2.2.2.2 -m comment --comment "None: route A zone: A location2 -> Anywhere: A location" 26 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:133 test) 27 | -A FORWARD -j REJECT --out-interface eth0 -m iprange --src-range 3.1.1.1-3.2.2.2 -m comment --comment "None: route any -> A zone: A location2" 28 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:141 test) 29 | -A FORWARD -j LOG --prefix "FWD None N" -s 1.1.1.1,2.2.2.2 -m iprange --src-range 1.1.1.1-2.2.2.2 -p tcp -m multiport --dports 2 --sports 1 -m comment --comment "None, channel tcp, ports 1 -> 2, plus --source 1.1.1.1,2.2.2.2, -m iprange --src-range 1.1.1.1-2.2.2.2" 30 | -A FORWARD -j LOG --prefix "FWD None N" -s 1.1.1.1,2.2.2.2 -m iprange --src-range 1.1.1.1-2.2.2.2 -p udp -m conntrack --ctstate ESTABLISHED -m comment --comment "None, channel udp, ESTABLISHED, ports any -> any, plus -s 1.1.1.1,2.2.2.2, -m iprange --src-range 1.1.1.1-2.2.2.2" 31 | -A FORWARD -j LOG --prefix "FWD None N" -s 1.1.1.1,2.2.2.2 -m iprange --src-range 1.1.1.1-2.2.2.2 -p icmp --icmp-type 1 -m comment --comment "None, channel icmp, type 1, plus -s 1.1.1.1,2.2.2.2, -m iprange --src-range 1.1.1.1-2.2.2.2" 32 | -A FORWARD -j LOG --prefix "FWD None N" -s 1.1.1.1,2.2.2.2 -m iprange --src-range 1.1.1.1-2.2.2.2 -p icmp -m comment --comment "None, channel icmp, type any, plus -s 1.1.1.1,2.2.2.2, -m iprange --src-range 1.1.1.1-2.2.2.2" 33 | 34 | # Builtin Chain "OUTPUT" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:16 default_tables)" 35 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:78 test) 36 | -A OUTPUT a random string 37 | # Rule: this is a custom rule with a comment (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:79 test) 38 | -A OUTPUT a random string -m comment --comment "this is a custom rule with a comment" 39 | # Rule: this is a custom rule with a comment (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:80 test) 40 | -A OUTPUT a random string -m comment --comment "this is a custom rule with a comment" 41 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:81 test) 42 | -A OUTPUT -s 1.1.1.1 -j ACCEPT 43 | -A OUTPUT -j DROP 44 | 45 | # User Chain "test_chain" (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:61 test)" 46 | # A user chain 47 | # Rule: A Rule (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:65 test) 48 | -A test_chain -i eth0 -s 1.1.2.1 ! -d 1.1.1.2 -j DROP -m comment --comment "A Rule" 49 | 50 | COMMIT 51 | 52 | ########################################################################################### 53 | # nat table (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:21 default_tables) # 54 | ########################################################################################### 55 | *nat 56 | :PREROUTING ACCEPT [0:0] 57 | :OUTPUT ACCEPT [0:0] 58 | :POSTROUTING ACCEPT [0:0] 59 | 60 | # Builtin Chain "PREROUTING" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:19 default_tables)" 61 | # No rules 62 | 63 | # Builtin Chain "OUTPUT" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:20 default_tables)" 64 | # No rules 65 | 66 | # Builtin Chain "POSTROUTING" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:21 default_tables)" 67 | # No rules 68 | 69 | COMMIT 70 | 71 | ############################################################################################## 72 | # mangle table (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:28 default_tables) # 73 | ############################################################################################## 74 | *mangle 75 | :PREROUTING ACCEPT [0:0] 76 | :INPUT ACCEPT [0:0] 77 | :FORWARD ACCEPT [0:0] 78 | :OUTPUT ACCEPT [0:0] 79 | :POSTROUTING ACCEPT [0:0] 80 | 81 | # Builtin Chain "PREROUTING" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:24 default_tables)" 82 | # No rules 83 | 84 | # Builtin Chain "INPUT" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:25 default_tables)" 85 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:102 test) 86 | -A INPUT -j LOG --prefix "IN None D" --source 1.1.1.1,2.2.2.2 -m comment --comment "None: route Anywhere: A location -> any" 87 | -A INPUT -j LOG --prefix "IN None D" --in-interface eth0 -m iprange --src-range 3.1.1.1-3.2.2.2 -m comment --comment "None: route A zone: A location2 -> any" 88 | -A INPUT -j LOG --prefix "IN None D" --in-interface br0 -m physdev --physdev-in eth0 -m comment --comment "None: route A location3 -> any" 89 | -A INPUT -j LOG --prefix "IN None D" -m set --match-set a_set src -m comment --comment "None: route a_set -> any" 90 | -A INPUT -j DROP --source 1.1.1.1,2.2.2.2 -m comment --comment "None: route Anywhere: A location -> any" 91 | -A INPUT -j DROP --in-interface eth0 -m iprange --src-range 3.1.1.1-3.2.2.2 -m comment --comment "None: route A zone: A location2 -> any" 92 | -A INPUT -j DROP --in-interface br0 -m physdev --physdev-in eth0 -m comment --comment "None: route A location3 -> any" 93 | -A INPUT -j DROP -m set --match-set a_set src -m comment --comment "None: route a_set -> any" 94 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:115 test) 95 | -A INPUT -j ACCEPT -p tcp -m multiport --dports 2 --sports 1 -m comment --comment "None, channel tcp, ports 1 -> 2" 96 | -A INPUT -j ACCEPT -p udp -m conntrack --ctstate ESTABLISHED -m comment --comment "None, channel udp, ESTABLISHED, ports any -> any" 97 | -A INPUT -j ACCEPT -p icmp --icmp-type 1 -m comment --comment "None, channel icmp, type 1" 98 | -A INPUT -j ACCEPT -p icmp -m comment --comment "None, channel icmp, type any" 99 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:117 test) 100 | -A INPUT -j REJECT 101 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:118 test) 102 | 103 | 104 | # Builtin Chain "FORWARD" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:26 default_tables)" 105 | # No rules 106 | 107 | # Builtin Chain "OUTPUT" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:27 default_tables)" 108 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:82 test) 109 | -A OUTPUT -j MARK --set-mark 123 110 | # Rule: (/home/jamiec/stuff/python-pyptables/pyptables/test/__init__.py:94 test) 111 | -A OUTPUT --match mark --mark 12340 112 | 113 | # Builtin Chain "POSTROUTING" (/home/jamiec/stuff/python-pyptables/pyptables/__init__.py:28 default_tables)" 114 | # No rules 115 | 116 | COMMIT 117 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = PyPTables 3 | version = 1.0.7 4 | author = Jamie Cockburn 5 | author_email = jamie_cockburn@hotmail.co.uk 6 | license = LICENSE.txt 7 | description = Python package for generating Linux iptables configurations. 8 | keywords = iptables,, firewall 9 | url = https://github.com/daggaz/python-pyptables 10 | long_description = file: README.rst 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Programming Language :: Python :: 2.7 14 | Programming Language :: Python :: 3 15 | Topic :: System :: Networking :: Firewalls 16 | License :: OSI Approved :: GNU General Public License v2 (GPLv2) 17 | Operating System :: POSIX :: Linux 18 | 19 | [options] 20 | packages = find: 21 | install_requires = six; nose; coverage 22 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | 5 | subprocess.call([ 6 | 'nosetests', 7 | '--nocapture', 8 | '--with-coverage', 9 | '--cover-package', 10 | 'pyptables', 11 | '--cover-inclusive', 12 | '--cover-html', 13 | '--cover-erase', 14 | ]) 15 | --------------------------------------------------------------------------------