├── .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 |
--------------------------------------------------------------------------------