sous-chefs / firewall

Development repository for the firewall cookbook
https://supermarket.chef.io/cookbooks/firewall
Apache License 2.0
99 stars 150 forks source link

Support chains other than *filter in iptables #101

Closed agperson closed 8 years ago

agperson commented 9 years ago

For comprehensive iptables support, in addition to the filter chain, also support raw, nat, and mangle.

There is an attempt in helpers_iptables.rb (#L24) to support NAT, but this isn't actually compatible with iptables-restore. This change is going to require a bit of restructuring.

jaseeey commented 9 years ago

It would be great to see support for this, particularly when wanting to create rules such as -A FORWARD -j REJECT --reject-with icmp-host-prohibited.

timwelch commented 8 years ago

I had to get NAT working for my load balancer recipe. I needed all VIPs on the load balancer to send packets out through themselves, rather than the original IP of the host. After failing to get it working using the :raw option, I hacked together some changes to the cookbook to get it working. The reason my change is considered a hack is because of the converge of the ruleset. It's converged using a sort.uniq. Iptables looks at both nat and filter sections as different sections, each requiring their own COMMIT line. But the sort.uniq sees 2 commit lines and removes one. I hacked around it by modifying the COMMIT line to be COMMIT_NAT and COMMIT_FILTER in the defaultruleset, and right AFTER the sort.uniq, when it's running through the lines to inject them, I replace anything COMMIT* with COMMIT. Very hackish, but unless/until there is a different way to roll through these and make them uniq, excluding the COMMIT lines, this is what is needed... Ruby uniq converts anything/everything into a hash... Hashes can't contain anything but unique values. It's a great way to make something unique, but in this scenario, we need a function that would allow us to specify things we want to keep. Especially since we already incorporate line numbers so we have that to our advantage..

A better way might actually be to inject the default ruleset AFTER this sort.uniq takes place. Then we can simply sort the hash by value to get it back in the proper order, and everything is happy... Anyways... YMMV.

Basically, the command I needed support for was exactly this...

iptables -t nat -A POSTROUTING -o eth1 -p tcp -d 172.28.128.21 -j SNAT --to-source 172.28.128.6
iptables -t nat -A POSTROUTING -o eth1 -p tcp -d 172.28.128.22 -j SNAT --to-source 172.28.128.6

Where 172.28.128.21 and.22 are servers in the load balanced pool and 172.28.128.6 is the VIP on the load balancer. Using keepaliveD on the load balancer to add the IP to the eth1 interface, but the traffic always flows out of the first IP configured on the interface...

Now, my load balancer recipe calls the firewall_rule like so:

    firewall_rule "api1.vagrant.powerdms.net postrouting src nat to 172.28.128.6" do
      direction :post
      command :snat
      destination "172.28.128.21"
      dest_interface "eth1"
      protocol :tcp
      to_source "172.28.128.6"
    end
  end

    firewall_rule "api2.vagrant.powerdms.net postrouting src nat to 172.28.128.6" do
      direction :post
      command :snat
      destination "172.28.128.22"
      dest_interface "eth1"
      protocol :tcp
      to_source "172.28.128.6"
    end
  end

The iptables file then gets generated like so:


# position 1
*nat
# position 2
:PREROUTING ACCEPT
# position 3
:POSTROUTING ACCEPT
# position 50
-A POSTROUTING -d 172.28.128.21 -o eth1 -p tcp -m tcp -m comment --comment "api01.vagrant.powerdms.net postrouting src nat to 172.28.128.6" -j SNAT --to-source 172.28.128.6
-A POSTROUTING -d 172.28.128.22 -o eth1 -p tcp -m tcp -m comment --comment "api02.vagrant.powerdms.net postrouting src nat to 172.28.128.6" -j SNAT --to-source 172.28.128.6
# position 100
COMMIT
# position 101
*filter
# position 102
:INPUT DROP
# position 103
:FORWARD DROP
# position 104
:OUTPUT ACCEPT
# position 150
-A INPUT -p tcp -m tcp -m multiport --dports 22 -m comment --comment "allow world to ssh" -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -m comment --comment "established" -j ACCEPT
-A INPUT -p icmp -m comment --comment "Allow ICMP" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 80 -m comment --comment "http" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 443 -m comment --comment "web_https" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 6380 -m comment --comment "redis_rw" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 6388 -m comment --comment "redis_ro" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 9200 -m comment --comment "elasticsearch" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 55555 -m comment --comment "hirus_compare" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 55556 -m comment --comment "hirus_convert" -j ACCEPT
-A INPUT -p tcp -m tcp -m multiport --dports 55557 -m comment --comment "hirus_backup" -j ACCEPT
# position 200
COMMIT

And now the traffic destined for 172.28.128.6 gets sent out masked as the correct IP and the servers in the load balanced pool see traffic coming from the correct IP as they should, instead of from the original eth1 IP (172.28.128.2)...

On to the relevant file changes...

libraries/helpers.rb : When converging the uniq lines into contents, replace any line that has COMMIT_ with COMMIT.

         contents << "# position #{sorted_value}"
         rules.each do |k, v|
           next unless v == sorted_value
+          if k.include?("COMMIT_")
+            k = "COMMIT"
+          end
           contents << k
         end
       end

libraries/helpers_iptables.rb : Several things are changing here...


       CHAIN = { in: 'INPUT', out: 'OUTPUT', pre: 'PREROUTING', post: 'POSTROUTING' } unless defined? CHAIN # , nil => "FORWARD"}
-      TARGET = { allow: 'ACCEPT', reject: 'REJECT', deny: 'DROP', masquerade: 'MASQUERADE', redirect: 'REDIRECT', log: 'LOG --log-prefix "iptables: " --log-level 7' } unless defined? TARGET
+      TARGET = { allow: 'ACCEPT', reject: 'REJECT', deny: 'DROP', masquerade: 'MASQUERADE', redirect: 'REDIRECT', log: 'LOG --log-prefix "iptables: " --log-level 7', snat: 'SNAT' } unless defined? TARGET

       def build_firewall_rule(current_node, rule_resource, ipv6 = false)
         el5 = (current_node['platform'] == 'rhel' || current_node['platform'] == 'centos') && Gem::Dependency.new('', '~> 5.0').match?('', current_node['platform_version'])
 @@ -13,16 +13,19 @@ def build_firewall_rule(current_node, rule_resource, ipv6 = false)
         if rule_resource.raw
           firewall_rule = rule_resource.raw.strip
         else
          firewall_rule = '-A '

           if rule_resource.direction
            firewall_rule << "#{CHAIN[rule_resource.direction.to_sym]} "
           else
            firewall_rule << 'FORWARD '
           end

-          if [:pre, :post].include?(rule_resource.direction)
-            firewall_rule << '-t nat '
-          end
+          #if [:pre, :post].include?(rule_resource.direction)
+          #  firewall_rule << '-t nat '
+          #end
+

           # Iptables order of prameters is important here see example output below:
           # -A INPUT -s 1.2.3.4/32 -d 5.6.7.8/32 -i lo -p tcp -m tcp -m state --state NEW -m comment --comment "hello" -j DROP
 @@ -46,6 +49,10 @@ def build_firewall_rule(current_node, rule_resource, ipv6 = false)
           end

           firewall_rule << "-j #{TARGET[rule_resource.command.to_sym]} "
           firewall_rule << "--to-ports #{rule_resource.redirect_port} " if rule_resource.action == :redirect
+
+          # Adding to-source for iptables -t nat type of rules
+          firewall_rule << "--to-source #{rule_resource.to_source} " if rule_resource.command == :snat
+

           firewall_rule.strip!

 @@ -93,11 +100,16 @@ def iptables_default_allow!(new_resource)

       def default_ruleset(current_node)
         {
-          '*filter' => 1,
-          ":INPUT #{current_node['firewall']['iptables']['defaults'][:policy][:input]}" => 2,
-          ":FORWARD #{current_node['firewall']['iptables']['defaults'][:policy][:forward]}" => 3,
-          ":OUTPUT #{current_node['firewall']['iptables']['defaults'][:policy][:output]}" => 4,
-          'COMMIT' => 100
+          '*nat' => 1,
+          ':PREROUTING ACCEPT' => 2,
+          ':POSTROUTING ACCEPT' => 3,
+          ':OUTPUT ACCEPT' => 4,
+          'COMMIT_NAT' => 100,
+          '*filter' => 101,
+          ":INPUT #{current_node['firewall']['iptables']['defaults'][:policy][:input]}" => 102,
+          ":FORWARD #{current_node['firewall']['iptables']['defaults'][:policy][:forward]}" => 103,
+          ":OUTPUT #{current_node['firewall']['iptables']['defaults'][:policy][:output]}" => 104,
+          'COMMIT_FILTER' => 200
         }
       end

libraries/provider_firewall_rule_iptables.rb : Modified types.each do loop to check whether the command uses SNAT in the line, and if it does, it generates a line number using nat_position, instead of position. This gives us a proper ordering/numbering of all of our lines..

       types.each do |iptables_type|
         # build rules to apply with weight
         k = build_firewall_rule(node, new_resource, iptables_type == 'ip6tables')
-        v = new_resource.position
-
+        unless k.include?("SNAT")
+          v = new_resource.position
+        else
+          v = new_resource.nat_position
+        end
+        

libraries/resource_firewall_rule.rb :


     attribute(:firewall_name, kind_of: String, default: 'default')

-    attribute(:command, kind_of: Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log], default: :allow)
+    attribute(:command, kind_of: Symbol, equal_to: [:reject, :allow, :deny, :masquerade, :redirect, :log, :snat], default: :allow)

     attribute(:protocol, kind_of: [Integer, Symbol], default: :tcp,
                          callbacks: { 'must be either :tcp, :udp, :icmp, :\'ipv6-icmp\', :icmpv6, :none, or a valid IP protocol number' => lambda do |p|
 @@ -31,7 +31,9 @@ class Resource::FirewallRule < Chef::Resource::LWRPBase
     attribute(:dest_port, kind_of: [Integer, Array, Range])
     attribute(:dest_interface, kind_of: String)

-    attribute(:position, kind_of: Integer, default: 50)
+
+    attribute(:nat_position, kind_of: Integer, default: 50)
+    attribute(:position, kind_of: Integer, default: 150)
     attribute(:stateful, kind_of: [Symbol, Array])
     attribute(:redirect_port, kind_of: Integer)
     attribute(:description, kind_of: String, name_attribute: true)
 @@ -43,6 +45,9 @@ class Resource::FirewallRule < Chef::Resource::LWRPBase
     attribute(:program, kind_of: String)
     attribute(:service, kind_of: String)

+    # --to-source option for nat in iptables
+    attribute(:to_source, kind_of: String)
+
     # for when you just want to pass a raw rule
     attribute(:raw, kind_of: String)
   end
martinb3 commented 8 years ago

Thanks @timwelch. I've added a workaround similar to yours, and released v2.4.0. I think we also need to do some refactoring as I've described in the commit message. If you have any opinions on that, let me know :)