1"""
2Management of iptables
3======================
4
5This is an iptables-specific module designed to manage Linux firewalls. It is
6expected that this state module, and other system-specific firewall states, may
7at some point be deprecated in favor of a more generic ``firewall`` state.
8
9.. code-block:: yaml
10
11    httpd:
12      iptables.append:
13        - table: filter
14        - chain: INPUT
15        - jump: ACCEPT
16        - match: state
17        - connstate: NEW
18        - dport: 80
19        - protocol: tcp
20        - sport: 1025:65535
21        - save: True
22
23    httpd:
24      iptables.append:
25        - table: filter
26        - chain: INPUT
27        - jump: ACCEPT
28        - match:
29            - state
30            - comment
31        - comment: "Allow HTTP"
32        - connstate: NEW
33        - dport: 80
34        - protocol: tcp
35        - sport: 1025:65535
36        - save: True
37
38    httpd:
39      iptables.append:
40        - table: filter
41        - chain: INPUT
42        - jump: ACCEPT
43        - match:
44            - state
45            - comment
46        - comment: "Allow HTTP"
47        - connstate: NEW
48        - source: '127.0.0.1'
49        - dport: 80
50        - protocol: tcp
51        - sport: 1025:65535
52        - save: True
53
54    .. Invert Rule
55    httpd:
56      iptables.append:
57        - table: filter
58        - chain: INPUT
59        - jump: ACCEPT
60        - match:
61            - state
62            - comment
63        - comment: "Allow HTTP"
64        - connstate: NEW
65        - source: '! 127.0.0.1'
66        - dport: 80
67        - protocol: tcp
68        - sport: 1025:65535
69        - save: True
70
71    httpd:
72      iptables.append:
73        - table: filter
74        - chain: INPUT
75        - jump: ACCEPT
76        - match:
77            - state
78            - comment
79        - comment: "Allow HTTP"
80        - connstate: NEW
81        - source: 'not 127.0.0.1'
82        - dport: 80
83        - protocol: tcp
84        - sport: 1025:65535
85        - save: True
86
87    httpd:
88      iptables.append:
89        - table: filter
90        - family: ipv6
91        - chain: INPUT
92        - jump: ACCEPT
93        - match: state
94        - connstate: NEW
95        - dport: 80
96        - protocol: tcp
97        - sport: 1025:65535
98        - save: True
99
100    httpd:
101      iptables.append:
102        - table: filter
103        - family: ipv4
104        - chain: INPUT
105        - jump: ACCEPT
106        - match: state
107        - connstate: NEW
108        - dports:
109            - 80
110            - 443
111        - protocol: tcp
112        - sport: 1025:65535
113        - save: True
114
115    httpd:
116      iptables.insert:
117        - position: 1
118        - table: filter
119        - chain: INPUT
120        - jump: ACCEPT
121        - match: state
122        - connstate: NEW
123        - dport: 80
124        - protocol: tcp
125        - sport: 1025:65535
126        - save: True
127
128    httpd:
129      iptables.insert:
130        - position: 1
131        - table: filter
132        - family: ipv6
133        - chain: INPUT
134        - jump: ACCEPT
135        - match: state
136        - connstate: NEW
137        - dport: 80
138        - protocol: tcp
139        - sport: 1025:65535
140        - save: True
141
142    httpd:
143      iptables.delete:
144        - table: filter
145        - chain: INPUT
146        - jump: ACCEPT
147        - match: state
148        - connstate: NEW
149        - dport: 80
150        - protocol: tcp
151        - sport: 1025:65535
152        - save: True
153
154    httpd:
155      iptables.delete:
156        - position: 1
157        - table: filter
158        - chain: INPUT
159        - jump: ACCEPT
160        - match: state
161        - connstate: NEW
162        - dport: 80
163        - protocol: tcp
164        - sport: 1025:65535
165        - save: True
166
167    httpd:
168      iptables.delete:
169        - table: filter
170        - family: ipv6
171        - chain: INPUT
172        - jump: ACCEPT
173        - match: state
174        - connstate: NEW
175        - dport: 80
176        - protocol: tcp
177        - sport: 1025:65535
178        - save: True
179
180    default to accept:
181      iptables.set_policy:
182        - chain: INPUT
183        - policy: ACCEPT
184
185.. note::
186
187    Whereas iptables will accept ``-p``, ``--proto[c[o[l]]]`` as synonyms of
188    ``--protocol``, if ``--proto`` appears in an iptables command after the
189    appearance of ``-m policy``, it is interpreted as the ``--proto`` option of
190    the policy extension (see the iptables-extensions(8) man page).
191
192Example rules for IPSec policy:
193
194.. code-block:: yaml
195
196    accept_esp_in:
197      iptables.append:
198        - table: filter
199        - chain: INPUT
200        - jump: ACCEPT
201        - source: 10.20.0.0/24
202        - destination: 10.10.0.0/24
203        - in-interface: eth0
204        - match: policy
205        - dir: in
206        - pol: ipsec
207        - reqid: 1
208        - proto: esp
209    accept_esp_forward_in:
210      iptables.append:
211        - use:
212          - iptables: accept_esp_in
213        - chain: FORWARD
214
215    accept_esp_out:
216      iptables.append:
217        - table: filter
218        - chain: OUTPUT
219        - jump: ACCEPT
220        - source: 10.10.0.0/24
221        - destination: 10.20.0.0/24
222        - out-interface: eth0
223        - match: policy
224        - dir: out
225        - pol: ipsec
226        - reqid: 1
227        - proto: esp
228    accept_esp_forward_out:
229      iptables.append:
230        - use:
231          - iptables: accept_esp_out
232        - chain: FORWARD
233
234.. note::
235
236    Various functions of the ``iptables`` module use the ``--check`` option. If
237    the version of ``iptables`` on the target system does not include this
238    option, an alternate version of this check will be performed using the
239    output of iptables-save. This may have unintended consequences on legacy
240    releases of ``iptables``.
241"""
242from salt.state import STATE_INTERNAL_KEYWORDS as _STATE_INTERNAL_KEYWORDS
243
244
245def __virtual__():
246    """
247    Only load if the locale module is available in __salt__
248    """
249    if "iptables.version" in __salt__:
250        return True
251    return (False, "iptables module could not be loaded")
252
253
254def chain_present(name, table="filter", family="ipv4"):
255    """
256    .. versionadded:: 2014.1.0
257
258    Verify the chain is exist.
259
260    name
261        A user-defined chain name.
262
263    table
264        The table to own the chain.
265
266    family
267        Networking family, either ipv4 or ipv6
268    """
269
270    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
271
272    chain_check = __salt__["iptables.check_chain"](table, name, family)
273    if chain_check is True:
274        ret["result"] = True
275        ret["comment"] = "iptables {} chain is already exist in {} table for {}".format(
276            name, table, family
277        )
278        return ret
279
280    if __opts__["test"]:
281        ret["comment"] = "iptables {} chain in {} table needs to be set for {}".format(
282            name, table, family
283        )
284        return ret
285    command = __salt__["iptables.new_chain"](table, name, family)
286    if command is True:
287        ret["changes"] = {"locale": name}
288        ret["result"] = True
289        ret["comment"] = "iptables {} chain in {} table create success for {}".format(
290            name, table, family
291        )
292        return ret
293    else:
294        ret["result"] = False
295        ret["comment"] = "Failed to create {} chain in {} table: {} for {}".format(
296            name, table, command.strip(), family
297        )
298        return ret
299
300
301def chain_absent(name, table="filter", family="ipv4"):
302    """
303    .. versionadded:: 2014.1.0
304
305    Verify the chain is absent.
306
307    table
308        The table to remove the chain from
309
310    family
311        Networking family, either ipv4 or ipv6
312    """
313
314    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
315
316    chain_check = __salt__["iptables.check_chain"](table, name, family)
317    if not chain_check:
318        ret["result"] = True
319        ret[
320            "comment"
321        ] = "iptables {} chain is already absent in {} table for {}".format(
322            name, table, family
323        )
324        return ret
325    if __opts__["test"]:
326        ret["comment"] = "iptables {} chain in {} table needs to be removed {}".format(
327            name, table, family
328        )
329        return ret
330    flush_chain = __salt__["iptables.flush"](table, name, family)
331    if not flush_chain:
332        command = __salt__["iptables.delete_chain"](table, name, family)
333        if command is True:
334            ret["changes"] = {"locale": name}
335            ret["result"] = True
336            ret[
337                "comment"
338            ] = "iptables {} chain in {} table delete success for {}".format(
339                name, table, family
340            )
341        else:
342            ret["result"] = False
343            ret["comment"] = "Failed to delete {} chain in {} table: {} for {}".format(
344                name, table, command.strip(), family
345            )
346    else:
347        ret["result"] = False
348        ret["comment"] = "Failed to flush {} chain in {} table: {} for {}".format(
349            name, table, flush_chain.strip(), family
350        )
351    return ret
352
353
354def append(name, table="filter", family="ipv4", **kwargs):
355    """
356    .. versionadded:: 0.17.0
357
358    Add a rule to the end of the specified chain.
359
360    name
361        A user-defined name to call this rule by in another part of a state or
362        formula. This should not be an actual rule.
363
364    table
365        The table that owns the chain which should be modified
366
367    family
368        Network family, ipv4 or ipv6.
369
370    All other arguments are passed in with the same name as the long option
371    that would normally be used for iptables, with one exception: ``--state`` is
372    specified as `connstate` instead of `state` (not to be confused with
373    `ctstate`).
374
375    Jump options that doesn't take arguments should be passed in with an empty
376    string.
377    """
378    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
379
380    if "rules" in kwargs:
381        ret["changes"]["locale"] = []
382        comments = []
383        save = False
384        for rule in kwargs["rules"]:
385            if "rules" in rule:
386                del rule["rules"]
387            if "__agg__" in rule:
388                del rule["__agg__"]
389            if "save" in rule and rule["save"]:
390                save = True
391                if rule["save"] is not True:
392                    save_file = rule["save"]
393                else:
394                    save_file = True
395                rule["save"] = False
396            _ret = append(**rule)
397            if "locale" in _ret["changes"]:
398                ret["changes"]["locale"].append(_ret["changes"]["locale"])
399            comments.append(_ret["comment"])
400            ret["result"] = _ret["result"]
401        if save:
402            if save_file is True:
403                save_file = None
404            __salt__["iptables.save"](filename=save_file, family=family)
405        if not ret["changes"]["locale"]:
406            del ret["changes"]["locale"]
407        ret["comment"] = "\n".join(comments)
408        return ret
409
410    for ignore in _STATE_INTERNAL_KEYWORDS:
411        if ignore in kwargs:
412            del kwargs[ignore]
413    kwargs["name"] = name
414    kwargs["table"] = table
415    rule = __salt__["iptables.build_rule"](family=family, **kwargs)
416    command = __salt__["iptables.build_rule"](
417        full="True", family=family, command="A", **kwargs
418    )
419    if __salt__["iptables.check"](table, kwargs["chain"], rule, family) is True:
420        ret["result"] = True
421        ret["comment"] = "iptables rule for {} already set ({}) for {}".format(
422            name, command.strip(), family
423        )
424        if "save" in kwargs and kwargs["save"]:
425            if kwargs["save"] is not True:
426                filename = kwargs["save"]
427            else:
428                filename = None
429            saved_rules = __salt__["iptables.get_saved_rules"](
430                conf_file=filename, family=family
431            )
432            _rules = __salt__["iptables.get_rules"](family=family)
433            __rules = []
434            for table in _rules:
435                for chain in _rules[table]:
436                    __rules.append(_rules[table][chain].get("rules"))
437            __saved_rules = []
438            for table in saved_rules:
439                for chain in saved_rules[table]:
440                    __saved_rules.append(saved_rules[table][chain].get("rules"))
441            # Only save if rules in memory are different than saved rules
442            if __rules != __saved_rules:
443                out = __salt__["iptables.save"](filename=filename, family=family)
444                ret["comment"] += "\nSaved iptables rule {} for {}\n{}\n{}".format(
445                    name, family, command.strip(), out
446                )
447        return ret
448    if __opts__["test"]:
449        ret["comment"] = "iptables rule for {} needs to be set ({}) for {}".format(
450            name, command.strip(), family
451        )
452        return ret
453    if __salt__["iptables.append"](table, kwargs["chain"], rule, family):
454        ret["changes"] = {"locale": name}
455        ret["result"] = True
456        ret["comment"] = "Set iptables rule for {} to: {} for {}".format(
457            name, command.strip(), family
458        )
459        if "save" in kwargs and kwargs["save"]:
460            if kwargs["save"] is not True:
461                filename = kwargs["save"]
462            else:
463                filename = None
464            out = __salt__["iptables.save"](filename=filename, family=family)
465            ret["comment"] = "Set and saved iptables rule {} for {}\n{}\n{}".format(
466                name, family, command.strip(), out
467            )
468        return ret
469    else:
470        ret["result"] = False
471        ret[
472            "comment"
473        ] = "Failed to set iptables rule for {}.\nAttempted rule was {} for {}".format(
474            name, command.strip(), family
475        )
476        return ret
477
478
479def insert(name, table="filter", family="ipv4", **kwargs):
480    """
481    .. versionadded:: 2014.1.0
482
483    Insert a rule into a chain
484
485    name
486        A user-defined name to call this rule by in another part of a state or
487        formula. This should not be an actual rule.
488
489    table
490        The table that owns the chain that should be modified
491
492    family
493        Networking family, either ipv4 or ipv6
494
495    position
496        The numerical representation of where the rule should be inserted into
497        the chain. Note that ``-1`` is not a supported position value.
498
499    All other arguments are passed in with the same name as the long option
500    that would normally be used for iptables, with one exception: ``--state`` is
501    specified as `connstate` instead of `state` (not to be confused with
502    `ctstate`).
503
504    Jump options that doesn't take arguments should be passed in with an empty
505    string.
506    """
507    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
508
509    if "rules" in kwargs:
510        ret["changes"]["locale"] = []
511        comments = []
512        save = False
513        for rule in kwargs["rules"]:
514            if "rules" in rule:
515                del rule["rules"]
516            if "__agg__" in rule:
517                del rule["__agg__"]
518            if "save" in rule and rule["save"]:
519                save = True
520                if rule["save"] is not True:
521                    save_file = rule["save"]
522                else:
523                    save_file = True
524                rule["save"] = False
525            _ret = insert(**rule)
526            if "locale" in _ret["changes"]:
527                ret["changes"]["locale"].append(_ret["changes"]["locale"])
528            comments.append(_ret["comment"])
529            ret["result"] = _ret["result"]
530        if save:
531            if save_file is True:
532                save_file = None
533            __salt__["iptables.save"](filename=save_file, family=family)
534        if not ret["changes"]["locale"]:
535            del ret["changes"]["locale"]
536        ret["comment"] = "\n".join(comments)
537        return ret
538
539    for ignore in _STATE_INTERNAL_KEYWORDS:
540        if ignore in kwargs:
541            del kwargs[ignore]
542    kwargs["name"] = name
543    kwargs["table"] = table
544    rule = __salt__["iptables.build_rule"](family=family, **kwargs)
545    command = __salt__["iptables.build_rule"](
546        full=True, family=family, command="I", **kwargs
547    )
548    if __salt__["iptables.check"](table, kwargs["chain"], rule, family) is True:
549        ret["result"] = True
550        ret["comment"] = "iptables rule for {} already set for {} ({})".format(
551            name, family, command.strip()
552        )
553        if "save" in kwargs and kwargs["save"]:
554            if kwargs["save"] is not True:
555                filename = kwargs["save"]
556            else:
557                filename = None
558            saved_rules = __salt__["iptables.get_saved_rules"](
559                conf_file=filename, family=family
560            )
561            _rules = __salt__["iptables.get_rules"](family=family)
562            __rules = []
563            for table in _rules:
564                for chain in _rules[table]:
565                    __rules.append(_rules[table][chain].get("rules"))
566            __saved_rules = []
567            for table in saved_rules:
568                for chain in saved_rules[table]:
569                    __saved_rules.append(saved_rules[table][chain].get("rules"))
570            # Only save if rules in memory are different than saved rules
571            if __rules != __saved_rules:
572                out = __salt__["iptables.save"](filename=filename, family=family)
573                ret["comment"] += "\nSaved iptables rule {} for {}\n{}\n{}".format(
574                    name, family, command.strip(), out
575                )
576        return ret
577    if __opts__["test"]:
578        ret["comment"] = "iptables rule for {} needs to be set for {} ({})".format(
579            name, family, command.strip()
580        )
581        return ret
582    if not __salt__["iptables.insert"](
583        table, kwargs["chain"], kwargs["position"], rule, family
584    ):
585        ret["changes"] = {"locale": name}
586        ret["result"] = True
587        ret["comment"] = "Set iptables rule for {} to: {} for {}".format(
588            name, command.strip(), family
589        )
590        if "save" in kwargs and kwargs["save"]:
591            if kwargs["save"] is not True:
592                filename = kwargs["save"]
593            else:
594                filename = None
595            out = __salt__["iptables.save"](filename=filename, family=family)
596            ret["comment"] = "Set and saved iptables rule {} for {}\n{}\n{}".format(
597                name, family, command.strip(), out
598            )
599        return ret
600    else:
601        ret["result"] = False
602        ret[
603            "comment"
604        ] = "Failed to set iptables rule for {}.\nAttempted rule was {}".format(
605            name, command.strip()
606        )
607        return ret
608
609
610def delete(name, table="filter", family="ipv4", **kwargs):
611    """
612    .. versionadded:: 2014.1.0
613
614    Delete a rule to a chain
615
616    name
617        A user-defined name to call this rule by in another part of a state or
618        formula. This should not be an actual rule.
619
620    table
621        The table that owns the chain that should be modified
622
623    family
624        Networking family, either ipv4 or ipv6
625
626    All other arguments are passed in with the same name as the long option
627    that would normally be used for iptables, with one exception: ``--state`` is
628    specified as `connstate` instead of `state` (not to be confused with
629    `ctstate`).
630
631    Jump options that doesn't take arguments should be passed in with an empty
632    string.
633    """
634    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
635
636    if "rules" in kwargs:
637        ret["changes"]["locale"] = []
638        comments = []
639        save = False
640        for rule in kwargs["rules"]:
641            if "rules" in rule:
642                del rule["rules"]
643            if "__agg__" in rule:
644                del rule["__agg__"]
645            if "save" in rule and rule["save"]:
646                if rule["save"] is not True:
647                    save_file = rule["save"]
648                else:
649                    save_file = True
650                rule["save"] = False
651            _ret = delete(**rule)
652            if "locale" in _ret["changes"]:
653                ret["changes"]["locale"].append(_ret["changes"]["locale"])
654            comments.append(_ret["comment"])
655            ret["result"] = _ret["result"]
656        if save:
657            if save_file is True:
658                save_file = None
659            __salt__["iptables.save"](filename=save_file, family=family)
660        if not ret["changes"]["locale"]:
661            del ret["changes"]["locale"]
662        ret["comment"] = "\n".join(comments)
663        return ret
664
665    for ignore in _STATE_INTERNAL_KEYWORDS:
666        if ignore in kwargs:
667            del kwargs[ignore]
668    kwargs["name"] = name
669    kwargs["table"] = table
670    rule = __salt__["iptables.build_rule"](family=family, **kwargs)
671    command = __salt__["iptables.build_rule"](
672        full=True, family=family, command="D", **kwargs
673    )
674
675    if not __salt__["iptables.check"](table, kwargs["chain"], rule, family) is True:
676        if "position" not in kwargs:
677            ret["result"] = True
678            ret["comment"] = "iptables rule for {} already absent for {} ({})".format(
679                name, family, command.strip()
680            )
681            return ret
682    if __opts__["test"]:
683        ret["comment"] = "iptables rule for {} needs to be deleted for {} ({})".format(
684            name, family, command.strip()
685        )
686        return ret
687
688    if "position" in kwargs:
689        result = __salt__["iptables.delete"](
690            table, kwargs["chain"], family=family, position=kwargs["position"]
691        )
692    else:
693        result = __salt__["iptables.delete"](
694            table, kwargs["chain"], family=family, rule=rule
695        )
696
697    if not result:
698        ret["changes"] = {"locale": name}
699        ret["result"] = True
700        ret["comment"] = "Delete iptables rule for {} {}".format(name, command.strip())
701        if "save" in kwargs and kwargs["save"]:
702            if kwargs["save"] is not True:
703                filename = kwargs["save"]
704            else:
705                filename = None
706            out = __salt__["iptables.save"](filename=filename, family=family)
707            ret["comment"] = "Deleted and saved iptables rule {} for {}\n{}\n{}".format(
708                name, family, command.strip(), out
709            )
710        return ret
711    else:
712        ret["result"] = False
713        ret[
714            "comment"
715        ] = "Failed to delete iptables rule for {}.\nAttempted rule was {}".format(
716            name, command.strip()
717        )
718        return ret
719
720
721def set_policy(name, table="filter", family="ipv4", **kwargs):
722    """
723    .. versionadded:: 2014.1.0
724
725    Sets the default policy for iptables firewall tables
726
727    table
728        The table that owns the chain that should be modified
729
730    family
731        Networking family, either ipv4 or ipv6
732
733    policy
734        The requested table policy
735
736    """
737    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
738
739    for ignore in _STATE_INTERNAL_KEYWORDS:
740        if ignore in kwargs:
741            del kwargs[ignore]
742
743    if (
744        __salt__["iptables.get_policy"](table, kwargs["chain"], family)
745        == kwargs["policy"]
746    ):
747        ret["result"] = True
748        ret[
749            "comment"
750        ] = "iptables default policy for chain {} on table {} for {} already set to {}".format(
751            kwargs["chain"], table, family, kwargs["policy"]
752        )
753        return ret
754    if __opts__["test"]:
755        ret["comment"] = (
756            "iptables default policy for chain {} on table {} for {} needs to be set"
757            " to {}".format(kwargs["chain"], table, family, kwargs["policy"])
758        )
759        return ret
760    if not __salt__["iptables.set_policy"](
761        table, kwargs["chain"], kwargs["policy"], family
762    ):
763        ret["changes"] = {"locale": name}
764        ret["result"] = True
765        ret["comment"] = "Set default policy for {} to {} family {}".format(
766            kwargs["chain"], kwargs["policy"], family
767        )
768        if "save" in kwargs and kwargs["save"]:
769            if kwargs["save"] is not True:
770                filename = kwargs["save"]
771            else:
772                filename = None
773            __salt__["iptables.save"](filename=filename, family=family)
774            ret[
775                "comment"
776            ] = "Set and saved default policy for {} to {} family {}".format(
777                kwargs["chain"], kwargs["policy"], family
778            )
779        return ret
780    else:
781        ret["result"] = False
782        ret["comment"] = "Failed to set iptables default policy"
783        return ret
784
785
786def flush(name, table="filter", family="ipv4", **kwargs):
787    """
788    .. versionadded:: 2014.1.0
789
790    Flush current iptables state
791
792    table
793        The table that owns the chain that should be modified
794
795    family
796        Networking family, either ipv4 or ipv6
797
798    chain
799        The chain to be flushed. All the chains in the table if none is given.
800
801
802    """
803    ret = {"name": name, "changes": {}, "result": None, "comment": ""}
804
805    for ignore in _STATE_INTERNAL_KEYWORDS:
806        if ignore in kwargs:
807            del kwargs[ignore]
808
809    if "chain" not in kwargs:
810        kwargs["chain"] = ""
811    if __opts__["test"]:
812        ret[
813            "comment"
814        ] = "iptables rules in {} table {} chain {} family needs to be flushed".format(
815            name, table, family
816        )
817        return ret
818    if not __salt__["iptables.flush"](table, kwargs["chain"], family):
819        ret["changes"] = {"locale": name}
820        ret["result"] = True
821        ret["comment"] = "Flush iptables rules in {} table {} chain {} family".format(
822            table, kwargs["chain"], family
823        )
824        return ret
825    else:
826        ret["result"] = False
827        ret["comment"] = "Failed to flush iptables rules"
828        return ret
829
830
831def mod_aggregate(low, chunks, running):
832    """
833    The mod_aggregate function which looks up all rules in the available
834    low chunks and merges them into a single rules ref in the present low data
835    """
836    rules = []
837    agg_enabled = [
838        "append",
839        "insert",
840    ]
841    if low.get("fun") not in agg_enabled:
842        return low
843    for chunk in chunks:
844        tag = __utils__["state.gen_tag"](chunk)
845        if tag in running:
846            # Already ran the iptables state, skip aggregation
847            continue
848        if chunk.get("state") == "iptables":
849            if "__agg__" in chunk:
850                continue
851            # Check for the same function
852            if chunk.get("fun") != low.get("fun"):
853                continue
854
855            if chunk not in rules:
856                rules.append(chunk)
857                chunk["__agg__"] = True
858
859    if rules:
860        if "rules" in low:
861            low["rules"].extend(rules)
862        else:
863            low["rules"] = rules
864    return low
865