1""" 2Management of firewalld 3 4.. versionadded:: 2015.8.0 5 6The following example applies changes to the public zone, blocks echo-reply 7and echo-request packets, does not set the zone to be the default, enables 8masquerading, and allows ports 22/tcp and 25/tcp. 9It will be applied permanently and directly before restart/reload. 10 11.. code-block:: yaml 12 13 public: 14 firewalld.present: 15 - name: public 16 - block_icmp: 17 - echo-reply 18 - echo-request 19 - default: False 20 - masquerade: True 21 - ports: 22 - 22/tcp 23 - 25/tcp 24 25The following example applies changes to the public zone, enables 26masquerading and configures port forwarding TCP traffic from port 22 27to 2222, and forwards TCP traffic from port 80 to 443 at 192.168.0.1. 28 29.. code-block:: yaml 30 31 my_zone: 32 firewalld.present: 33 - name: public 34 - masquerade: True 35 - port_fwd: 36 - 22:2222:tcp 37 - 80:443:tcp:192.168.0.1 38 39The following example binds the public zone to interface eth0 and to all 40packets coming from the 192.168.1.0/24 subnet. It also removes the zone 41from all other interfaces or sources. 42 43.. code-block:: yaml 44 45 public: 46 firewalld.present: 47 - name: public 48 - interfaces: 49 - eth0 50 - sources: 51 - 192.168.1.0/24 52 53Here, we define a new service that encompasses TCP ports 4505 4506: 54 55.. code-block:: yaml 56 57 saltmaster: 58 firewalld.service: 59 - name: saltmaster 60 - ports: 61 - 4505/tcp 62 - 4506/tcp 63 64To make this new service available in a zone, the following can be used, which 65would allow access to the salt master from the 10.0.0.0/8 subnet: 66 67.. code-block:: yaml 68 69 saltzone: 70 firewalld.present: 71 - name: saltzone 72 - services: 73 - saltmaster 74 - sources: 75 - 10.0.0.0/8 76 77Another way of implementing the same rule above using rich rules is demonstrated 78here: 79 80.. code-block:: yaml 81 82 saltzone: 83 firewalld.present: 84 - name: saltzone 85 - rich_rules: 86 - rule service name="saltmaster" accept 87 - sources: 88 - 10.0.0.0/8 89 90The format of rich rules is the same as: 91 92.. code-block:: shell 93 94 firewall-cmd --list-rich-rules 95 96with an example output of: 97 98.. code-block:: text 99 100 rule protocol value="icmp" accept 101 rule protocol value="ipv6-icmp" accept 102 rule service name="snmp" accept 103""" 104 105 106import logging 107 108import salt.utils.path 109from salt.exceptions import CommandExecutionError 110from salt.output import nested 111 112log = logging.getLogger(__name__) 113 114 115class ForwardingMapping: 116 """ 117 Represents a port forwarding statement mapping a local port to a remote 118 port for a specific protocol (TCP or UDP) 119 """ 120 121 def __init__(self, srcport, destport, protocol, destaddr): 122 self.srcport = srcport 123 self.destport = destport 124 self.protocol = protocol 125 self.destaddr = destaddr 126 127 def __eq__(self, other): 128 return ( 129 self.srcport == other.srcport 130 and self.destport == other.destport 131 and self.protocol == other.protocol 132 and self.destaddr == other.destaddr 133 ) 134 135 def __ne__(self, other): 136 return not self.__eq__(other) 137 138 # hash is needed for set operations 139 def __hash__(self): 140 return ( 141 hash(self.srcport) 142 ^ hash(self.destport) 143 ^ hash(self.protocol) 144 ^ hash(self.destaddr) 145 ) 146 147 def todict(self): 148 """ 149 Returns a pretty dictionary meant for command line output. 150 """ 151 return { 152 "Source port": self.srcport, 153 "Destination port": self.destport, 154 "Protocol": self.protocol, 155 "Destination address": self.destaddr, 156 } 157 158 159def _parse_forward(mapping): 160 """ 161 Parses a port forwarding statement in the form used by this state: 162 163 from_port:to_port:protocol[:destination] 164 165 and returns a ForwardingMapping object 166 """ 167 if len(mapping.split(":")) > 3: 168 (srcport, destport, protocol, destaddr) = mapping.split(":") 169 else: 170 (srcport, destport, protocol) = mapping.split(":") 171 destaddr = "" 172 return ForwardingMapping(srcport, destport, protocol, destaddr) 173 174 175def __virtual__(): 176 """ 177 Ensure the firewall-cmd is available 178 """ 179 if salt.utils.path.which("firewall-cmd"): 180 return True 181 182 return ( 183 False, 184 "firewall-cmd is not available, firewalld is probably not installed.", 185 ) 186 187 188def present( 189 name, 190 block_icmp=None, 191 prune_block_icmp=False, 192 default=None, 193 masquerade=False, 194 ports=None, 195 prune_ports=False, 196 port_fwd=None, 197 prune_port_fwd=False, 198 services=None, 199 prune_services=False, 200 interfaces=None, 201 prune_interfaces=False, 202 sources=None, 203 prune_sources=False, 204 rich_rules=None, 205 prune_rich_rules=False, 206): 207 208 """ 209 Ensure a zone has specific attributes. 210 211 name 212 The zone to modify. 213 214 default : None 215 Set this zone as the default zone if ``True``. 216 217 masquerade : False 218 Enable or disable masquerade for a zone. 219 220 block_icmp : None 221 List of ICMP types to block in the zone. 222 223 prune_block_icmp : False 224 If ``True``, remove all but the specified block_icmp from the zone. 225 226 ports : None 227 List of ports to add to the zone. 228 229 prune_ports : False 230 If ``True``, remove all but the specified ports from the zone. 231 232 port_fwd : None 233 List of port forwards to add to the zone. 234 235 prune_port_fwd : False 236 If ``True``, remove all but the specified port_fwd from the zone. 237 238 services : None 239 List of services to add to the zone. 240 241 prune_services : False 242 If ``True``, remove all but the specified services from the zone. 243 .. note:: Currently defaults to True for compatibility, but will be changed to False in a future release. 244 245 interfaces : None 246 List of interfaces to add to the zone. 247 248 prune_interfaces : False 249 If ``True``, remove all but the specified interfaces from the zone. 250 251 sources : None 252 List of sources to add to the zone. 253 254 prune_sources : False 255 If ``True``, remove all but the specified sources from the zone. 256 257 rich_rules : None 258 List of rich rules to add to the zone. 259 260 prune_rich_rules : False 261 If ``True``, remove all but the specified rich rules from the zone. 262 """ 263 ret = _present( 264 name, 265 block_icmp, 266 prune_block_icmp, 267 default, 268 masquerade, 269 ports, 270 prune_ports, 271 port_fwd, 272 prune_port_fwd, 273 services, 274 prune_services, 275 interfaces, 276 prune_interfaces, 277 sources, 278 prune_sources, 279 rich_rules, 280 prune_rich_rules, 281 ) 282 283 # Reload firewalld service on changes 284 if ret["changes"] != {}: 285 __salt__["firewalld.reload_rules"]() 286 287 return ret 288 289 290def service(name, ports=None, protocols=None): 291 """ 292 Ensure the service exists and encompasses the specified ports and 293 protocols. 294 295 .. versionadded:: 2016.11.0 296 """ 297 ret = {"name": name, "result": False, "changes": {}, "comment": ""} 298 299 if name not in __salt__["firewalld.get_services"](): 300 __salt__["firewalld.new_service"](name, restart=False) 301 302 ports = ports or [] 303 304 try: 305 _current_ports = __salt__["firewalld.get_service_ports"](name) 306 except CommandExecutionError as err: 307 ret["comment"] = "Error: {}".format(err) 308 return ret 309 310 new_ports = set(ports) - set(_current_ports) 311 old_ports = set(_current_ports) - set(ports) 312 313 for port in new_ports: 314 if not __opts__["test"]: 315 try: 316 __salt__["firewalld.add_service_port"](name, port) 317 except CommandExecutionError as err: 318 ret["comment"] = "Error: {}".format(err) 319 return ret 320 321 for port in old_ports: 322 if not __opts__["test"]: 323 try: 324 __salt__["firewalld.remove_service_port"](name, port) 325 except CommandExecutionError as err: 326 ret["comment"] = "Error: {}".format(err) 327 return ret 328 329 if new_ports or old_ports: 330 ret["changes"].update({"ports": {"old": _current_ports, "new": ports}}) 331 332 protocols = protocols or [] 333 334 try: 335 _current_protocols = __salt__["firewalld.get_service_protocols"](name) 336 except CommandExecutionError as err: 337 ret["comment"] = "Error: {}".format(err) 338 return ret 339 340 new_protocols = set(protocols) - set(_current_protocols) 341 old_protocols = set(_current_protocols) - set(protocols) 342 343 for protocol in new_protocols: 344 if not __opts__["test"]: 345 try: 346 __salt__["firewalld.add_service_protocol"](name, protocol) 347 except CommandExecutionError as err: 348 ret["comment"] = "Error: {}".format(err) 349 return ret 350 351 for protocol in old_protocols: 352 if not __opts__["test"]: 353 try: 354 __salt__["firewalld.remove_service_protocol"](name, protocol) 355 except CommandExecutionError as err: 356 ret["comment"] = "Error: {}".format(err) 357 return ret 358 359 if new_protocols or old_protocols: 360 ret["changes"].update( 361 {"protocols": {"old": _current_protocols, "new": protocols}} 362 ) 363 364 if ret["changes"] != {}: 365 __salt__["firewalld.reload_rules"]() 366 367 ret["result"] = True 368 if ret["changes"] == {}: 369 ret["comment"] = "'{}' is already in the desired state.".format(name) 370 return ret 371 372 if __opts__["test"]: 373 ret["result"] = None 374 ret["comment"] = "Configuration for '{}' will change.".format(name) 375 return ret 376 377 ret["comment"] = "'{}' was configured.".format(name) 378 return ret 379 380 381def _present( 382 name, 383 block_icmp=None, 384 prune_block_icmp=False, 385 default=None, 386 masquerade=False, 387 ports=None, 388 prune_ports=False, 389 port_fwd=None, 390 prune_port_fwd=False, 391 services=None, 392 # TODO: prune_services=False in future release 393 # prune_services=False, 394 prune_services=None, 395 interfaces=None, 396 prune_interfaces=False, 397 sources=None, 398 prune_sources=False, 399 rich_rules=None, 400 prune_rich_rules=False, 401): 402 """ 403 Ensure a zone has specific attributes. 404 """ 405 ret = {"name": name, "result": False, "changes": {}, "comment": ""} 406 407 try: 408 zones = __salt__["firewalld.get_zones"](permanent=True) 409 except CommandExecutionError as err: 410 ret["comment"] = "Error: {}".format(err) 411 return ret 412 413 if name not in zones: 414 if not __opts__["test"]: 415 try: 416 __salt__["firewalld.new_zone"](name) 417 except CommandExecutionError as err: 418 ret["comment"] = "Error: {}".format(err) 419 return ret 420 421 ret["changes"].update({name: {"old": zones, "new": name}}) 422 423 if block_icmp or prune_block_icmp: 424 block_icmp = block_icmp or [] 425 new_icmp_types = [] 426 old_icmp_types = [] 427 428 try: 429 _current_icmp_blocks = __salt__["firewalld.list_icmp_block"]( 430 name, permanent=True 431 ) 432 except CommandExecutionError as err: 433 ret["comment"] = "Error: {}".format(err) 434 return ret 435 436 if block_icmp: 437 try: 438 _valid_icmp_types = __salt__["firewalld.get_icmp_types"](permanent=True) 439 except CommandExecutionError as err: 440 ret["comment"] = "Error: {}".format(err) 441 return ret 442 443 # log errors for invalid ICMP types in block_icmp input 444 for icmp_type in set(block_icmp) - set(_valid_icmp_types): 445 log.error("%s is an invalid ICMP type", icmp_type) 446 block_icmp.remove(icmp_type) 447 448 new_icmp_types = set(block_icmp) - set(_current_icmp_blocks) 449 for icmp_type in new_icmp_types: 450 if not __opts__["test"]: 451 try: 452 __salt__["firewalld.block_icmp"]( 453 name, icmp_type, permanent=True 454 ) 455 except CommandExecutionError as err: 456 ret["comment"] = "Error: {}".format(err) 457 return ret 458 459 if prune_block_icmp: 460 old_icmp_types = set(_current_icmp_blocks) - set(block_icmp) 461 for icmp_type in old_icmp_types: 462 # no need to check against _valid_icmp_types here, because all 463 # elements in old_icmp_types are guaranteed to be in 464 # _current_icmp_blocks, whose elements are inherently valid 465 if not __opts__["test"]: 466 try: 467 __salt__["firewalld.allow_icmp"]( 468 name, icmp_type, permanent=True 469 ) 470 except CommandExecutionError as err: 471 ret["comment"] = "Error: {}".format(err) 472 return ret 473 474 if new_icmp_types or old_icmp_types: 475 # If we're not pruning, include current items in new output so it's clear 476 # that they're still present 477 if not prune_block_icmp: 478 block_icmp = list(new_icmp_types | set(_current_icmp_blocks)) 479 ret["changes"].update( 480 {"icmp_types": {"old": _current_icmp_blocks, "new": block_icmp}} 481 ) 482 483 # that's the only parameter that can't be permanent or runtime, it's 484 # directly both 485 if default: 486 try: 487 default_zone = __salt__["firewalld.default_zone"]() 488 except CommandExecutionError as err: 489 ret["comment"] = "Error: {}".format(err) 490 return ret 491 if name != default_zone: 492 if not __opts__["test"]: 493 try: 494 __salt__["firewalld.set_default_zone"](name) 495 except CommandExecutionError as err: 496 ret["comment"] = "Error: {}".format(err) 497 return ret 498 ret["changes"].update({"default": {"old": default_zone, "new": name}}) 499 500 try: 501 masquerade_ret = __salt__["firewalld.get_masquerade"](name, permanent=True) 502 except CommandExecutionError as err: 503 ret["comment"] = "Error: {}".format(err) 504 return ret 505 506 if masquerade and not masquerade_ret: 507 if not __opts__["test"]: 508 try: 509 __salt__["firewalld.add_masquerade"](name, permanent=True) 510 except CommandExecutionError as err: 511 ret["comment"] = "Error: {}".format(err) 512 return ret 513 ret["changes"].update( 514 {"masquerade": {"old": "", "new": "Masquerading successfully set."}} 515 ) 516 elif not masquerade and masquerade_ret: 517 if not __opts__["test"]: 518 try: 519 __salt__["firewalld.remove_masquerade"](name, permanent=True) 520 except CommandExecutionError as err: 521 ret["comment"] = "Error: {}".format(err) 522 return ret 523 ret["changes"].update( 524 {"masquerade": {"old": "", "new": "Masquerading successfully disabled."}} 525 ) 526 527 if ports or prune_ports: 528 ports = ports or [] 529 try: 530 _current_ports = __salt__["firewalld.list_ports"](name, permanent=True) 531 except CommandExecutionError as err: 532 ret["comment"] = "Error: {}".format(err) 533 return ret 534 535 new_ports = set(ports) - set(_current_ports) 536 old_ports = [] 537 538 for port in new_ports: 539 if not __opts__["test"]: 540 try: 541 __salt__["firewalld.add_port"]( 542 name, port, permanent=True, force_masquerade=False 543 ) 544 except CommandExecutionError as err: 545 ret["comment"] = "Error: {}".format(err) 546 return ret 547 548 if prune_ports: 549 old_ports = set(_current_ports) - set(ports) 550 for port in old_ports: 551 if not __opts__["test"]: 552 try: 553 __salt__["firewalld.remove_port"](name, port, permanent=True) 554 except CommandExecutionError as err: 555 ret["comment"] = "Error: {}".format(err) 556 return ret 557 558 if new_ports or old_ports: 559 # If we're not pruning, include current items in new output so it's clear 560 # that they're still present 561 if not prune_ports: 562 ports = list(new_ports | set(_current_ports)) 563 ret["changes"].update({"ports": {"old": _current_ports, "new": ports}}) 564 565 if port_fwd or prune_port_fwd: 566 port_fwd = port_fwd or [] 567 try: 568 _current_port_fwd = __salt__["firewalld.list_port_fwd"]( 569 name, permanent=True 570 ) 571 except CommandExecutionError as err: 572 ret["comment"] = "Error: {}".format(err) 573 return ret 574 575 port_fwd = [_parse_forward(fwd) for fwd in port_fwd] 576 _current_port_fwd = [ 577 ForwardingMapping( 578 srcport=fwd["Source port"], 579 destport=fwd["Destination port"], 580 protocol=fwd["Protocol"], 581 destaddr=fwd["Destination address"], 582 ) 583 for fwd in _current_port_fwd 584 ] 585 586 new_port_fwd = set(port_fwd) - set(_current_port_fwd) 587 old_port_fwd = [] 588 589 for fwd in new_port_fwd: 590 if not __opts__["test"]: 591 try: 592 __salt__["firewalld.add_port_fwd"]( 593 name, 594 fwd.srcport, 595 fwd.destport, 596 fwd.protocol, 597 fwd.destaddr, 598 permanent=True, 599 force_masquerade=False, 600 ) 601 except CommandExecutionError as err: 602 ret["comment"] = "Error: {}".format(err) 603 return ret 604 605 if prune_port_fwd: 606 old_port_fwd = set(_current_port_fwd) - set(port_fwd) 607 for fwd in old_port_fwd: 608 if not __opts__["test"]: 609 try: 610 __salt__["firewalld.remove_port_fwd"]( 611 name, 612 fwd.srcport, 613 fwd.destport, 614 fwd.protocol, 615 fwd.destaddr, 616 permanent=True, 617 ) 618 except CommandExecutionError as err: 619 ret["comment"] = "Error: {}".format(err) 620 return ret 621 622 if new_port_fwd or old_port_fwd: 623 # If we're not pruning, include current items in new output so it's clear 624 # that they're still present 625 if not prune_port_fwd: 626 port_fwd = list(new_port_fwd | set(_current_port_fwd)) 627 ret["changes"].update( 628 { 629 "port_fwd": { 630 "old": [fwd.todict() for fwd in _current_port_fwd], 631 "new": [fwd.todict() for fwd in port_fwd], 632 } 633 } 634 ) 635 636 if services or prune_services: 637 services = services or [] 638 try: 639 _current_services = __salt__["firewalld.list_services"]( 640 name, permanent=True 641 ) 642 except CommandExecutionError as err: 643 ret["comment"] = "Error: {}".format(err) 644 return ret 645 646 new_services = set(services) - set(_current_services) 647 old_services = [] 648 649 for new_service in new_services: 650 if not __opts__["test"]: 651 try: 652 __salt__["firewalld.add_service"](new_service, name, permanent=True) 653 except CommandExecutionError as err: 654 ret["comment"] = "Error: {}".format(err) 655 return ret 656 657 if prune_services: 658 old_services = set(_current_services) - set(services) 659 for old_service in old_services: 660 if not __opts__["test"]: 661 try: 662 __salt__["firewalld.remove_service"]( 663 old_service, name, permanent=True 664 ) 665 except CommandExecutionError as err: 666 ret["comment"] = "Error: {}".format(err) 667 return ret 668 669 if new_services or old_services: 670 # If we're not pruning, include current items in new output so it's clear 671 # that they're still present 672 if not prune_services: 673 services = list(new_services | set(_current_services)) 674 ret["changes"].update( 675 {"services": {"old": _current_services, "new": services}} 676 ) 677 678 if interfaces or prune_interfaces: 679 interfaces = interfaces or [] 680 try: 681 _current_interfaces = __salt__["firewalld.get_interfaces"]( 682 name, permanent=True 683 ) 684 except CommandExecutionError as err: 685 ret["comment"] = "Error: {}".format(err) 686 return ret 687 688 new_interfaces = set(interfaces) - set(_current_interfaces) 689 old_interfaces = [] 690 691 for interface in new_interfaces: 692 if not __opts__["test"]: 693 try: 694 __salt__["firewalld.add_interface"](name, interface, permanent=True) 695 except CommandExecutionError as err: 696 ret["comment"] = "Error: {}".format(err) 697 return ret 698 699 if prune_interfaces: 700 old_interfaces = set(_current_interfaces) - set(interfaces) 701 for interface in old_interfaces: 702 if not __opts__["test"]: 703 try: 704 __salt__["firewalld.remove_interface"]( 705 name, interface, permanent=True 706 ) 707 except CommandExecutionError as err: 708 ret["comment"] = "Error: {}".format(err) 709 return ret 710 711 if new_interfaces or old_interfaces: 712 # If we're not pruning, include current items in new output so it's clear 713 # that they're still present 714 if not prune_interfaces: 715 interfaces = list(new_interfaces | set(_current_interfaces)) 716 ret["changes"].update( 717 {"interfaces": {"old": _current_interfaces, "new": interfaces}} 718 ) 719 720 if sources or prune_sources: 721 sources = sources or [] 722 try: 723 _current_sources = __salt__["firewalld.get_sources"](name, permanent=True) 724 except CommandExecutionError as err: 725 ret["comment"] = "Error: {}".format(err) 726 return ret 727 728 new_sources = set(sources) - set(_current_sources) 729 old_sources = [] 730 731 for source in new_sources: 732 if not __opts__["test"]: 733 try: 734 __salt__["firewalld.add_source"](name, source, permanent=True) 735 except CommandExecutionError as err: 736 ret["comment"] = "Error: {}".format(err) 737 return ret 738 739 if prune_sources: 740 old_sources = set(_current_sources) - set(sources) 741 for source in old_sources: 742 if not __opts__["test"]: 743 try: 744 __salt__["firewalld.remove_source"]( 745 name, source, permanent=True 746 ) 747 except CommandExecutionError as err: 748 ret["comment"] = "Error: {}".format(err) 749 return ret 750 751 if new_sources or old_sources: 752 # If we're not pruning, include current items in new output so it's clear 753 # that they're still present 754 if not prune_sources: 755 sources = list(new_sources | set(_current_sources)) 756 ret["changes"].update( 757 {"sources": {"old": _current_sources, "new": sources}} 758 ) 759 760 if rich_rules or prune_rich_rules: 761 rich_rules = rich_rules or [] 762 try: 763 _current_rich_rules = __salt__["firewalld.get_rich_rules"]( 764 name, permanent=True 765 ) 766 except CommandExecutionError as err: 767 ret["comment"] = "Error: {}".format(err) 768 return ret 769 770 new_rich_rules = set(rich_rules) - set(_current_rich_rules) 771 old_rich_rules = [] 772 773 for rich_rule in new_rich_rules: 774 if not __opts__["test"]: 775 try: 776 __salt__["firewalld.add_rich_rule"](name, rich_rule, permanent=True) 777 except CommandExecutionError as err: 778 ret["comment"] = "Error: {}".format(err) 779 return ret 780 781 if prune_rich_rules: 782 old_rich_rules = set(_current_rich_rules) - set(rich_rules) 783 for rich_rule in old_rich_rules: 784 if not __opts__["test"]: 785 try: 786 __salt__["firewalld.remove_rich_rule"]( 787 name, rich_rule, permanent=True 788 ) 789 except CommandExecutionError as err: 790 ret["comment"] = "Error: {}".format(err) 791 return ret 792 793 if new_rich_rules or old_rich_rules: 794 # If we're not pruning, include current items in new output so it's clear 795 # that they're still present 796 if not prune_rich_rules: 797 rich_rules = list(new_rich_rules | set(_current_rich_rules)) 798 ret["changes"].update( 799 {"rich_rules": {"old": _current_rich_rules, "new": rich_rules}} 800 ) 801 802 # No changes 803 if ret["changes"] == {}: 804 ret["result"] = True 805 ret["comment"] = "'{}' is already in the desired state.".format(name) 806 return ret 807 808 # test=True and changes predicted 809 if __opts__["test"]: 810 ret["result"] = None 811 # build comment string 812 nested.__opts__ = __opts__ 813 comment = [] 814 comment.append("Configuration for '{}' will change:".format(name)) 815 comment.append(nested.output(ret["changes"]).rstrip()) 816 ret["comment"] = "\n".join(comment) 817 ret["changes"] = {} 818 return ret 819 820 # Changes were made successfully 821 ret["result"] = True 822 ret["comment"] = "'{}' was configured.".format(name) 823 return ret 824