# Native nft Security Groups

**URL:** https://forum.opennebula.io/t/native-nft-security-groups/8273
**Category:** Integration Support
**Created:** [March 31, 2020, 2:15pm UTC](https://forum.opennebula.io/t/native-nft-security-groups/8273 "2020-03-31T14:15:41Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![divanikus](https://avatars.discourse-cdn.com/v4/letter/d/8797f3/32.png) [@divanikus](https://forum.opennebula.io/u/divanikus)
#### Post date: [March 31, 2020, 2:15pm UTC](https://forum.opennebula.io/t/native-nft-security-groups/8273/1 "2020-03-31T14:15:42Z")

</div>

Hello.  
I have ported iptables security groups to newer nftables. All types of filters are supported.  
All operations are made on bridge family table, so unfortunatelly the resulting ruleset is stateless.

```
table bridge filter {
        chain INPUT {
                type filter hook input priority -200; policy accept;
                iifname "one-0-0" jump one-0-0-o
        }

        chain FORWARD {
                type filter hook forward priority -200; policy accept;
                oifname "one-0-0" jump one-0-0-i
                iifname "one-0-0" jump one-0-0-o
        }

        chain OUTPUT {
                type filter hook output priority -200; policy accept;
                oifname "one-0-0" jump one-0-0-i
        }

        chain one-0-0-i {
                ether type arp counter packets 4 bytes 112 accept
                icmpv6 type nd-router-advert counter packets 0 bytes 0 accept
                icmpv6 type nd-neighbor-solicit counter packets 6 bytes 432 accept
                icmpv6 type nd-neighbor-advert counter packets 0 bytes 0 accept
                icmpv6 type nd-redirect counter packets 0 bytes 0 accept
                counter packets 400 bytes 22226 return
                counter packets 0 bytes 0 drop
        }

        chain one-0-0-o {
                ether type arp counter packets 4 bytes 112 accept
                icmpv6 type nd-router-solicit counter packets 0 bytes 0 accept
                icmpv6 type nd-neighbor-solicit counter packets 0 bytes 0 accept
                icmpv6 type nd-neighbor-advert counter packets 0 bytes 0 accept
                ether saddr != 02:00:c0:a8:05:03 counter packets 0 bytes 0 drop
                counter packets 51 bytes 2566 return
                counter packets 0 bytes 0 drop
        }
}

```

Is anyone interested in such integration? How to properly integrate it with Opennebula?

---

<div class="post-metadata">

### Author: ![divanikus](https://avatars.discourse-cdn.com/v4/letter/d/8797f3/32.png) [@divanikus](https://forum.opennebula.io/u/divanikus)
#### Post date: [March 31, 2020, 2:17pm UTC](https://forum.opennebula.io/t/native-nft-security-groups/8273/2 "2020-03-31T14:17:54Z")

</div>

```ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2019, OpenNebula Project, OpenNebula Systems #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
# not use this file except in compliance with the License. You may obtain #
# a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
#--------------------------------------------------------------------------- #

module VNMMAD

# This module implements the SecurityGroup abstraction on top of nft
module SGNft

    ############################################################################
    # A Rule implemented with the nft Linux kernel facilities
    ############################################################################
    class RuleNft < VNMNetwork::Rule
        ########################################################################
        # Implementation of each rule type
        ########################################################################
        private

        # Implements the :protocol rule. Example:
        # nft add rule ip filter one-3-0-i meta l4proto tcp counter return
        def process_protocol(cmds, vars)
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]

            proto = @protocol == :all ? "" : "meta l4proto #{@protocol}"
            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} #{proto} counter return"
        end

        # Implements the :portrange rule. Example:
        # nft add rule ip filter one-3-0-o meta l4proto udp udp dport { 80,22 } counter return
        def process_portrange(cmds, vars)
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]

            range = @range.gsub(/:/, '-')
            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} meta l4proto #{@protocol}" \
                " #{@protocol} dport { #{range} } counter return"
        end

        # Implements the :icmp_type rule. Example:
        # nft add rule ip filter one-3-0-o icmp type 8 counter return
        def process_icmp_type(cmds, vars)
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]

            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} icmp type #{@icmp_type}"\
                " counter return"
        end

        # Implements the :icmpv6_type rule. Example:
        # nft add rule ip6 filter one-3-0-o meta l4proto ipv6-icmp icmpv6 type 128 counter return
        def process_icmpv6_type(cmds, vars)
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]

            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} meta l4proto ipv6-icmp icmpv6 type "\
                "#{@icmpv6_type} counter return"
        end

        # Implements the :net rule. Example:
        # nft add rule bridge filter one-3-0-i meta l4proto tcp \
        # ip saddr { 10.0.0.0/24 } counter return
        # comment \"one-3-0-1-i-tcp-n-ip\"
        def process_net(cmds, vars)
            the_nets = net()

            return if the_nets.empty?

            sets = {}
            proto = @protocol == :all ? "" : "meta l4proto #{@protocol}"

            the_nets.each do |n|
                if IPAddr.new(n).ipv6?
                    family = "ip6"
                else
                    family = "ip"
                end

                if @rule_type == :inbound
                    chain = vars[:chain_in]
                    set = "#{vars[:set_sg_in]}-#{@protocol}-n-#{family}"
                    dir = "#{family} saddr"
                else
                    chain = vars[:chain_out]
                    set = "#{vars[:set_sg_out]}-#{@protocol}-n-#{family}"
                    dir = "#{family} daddr"
                end

                sets[set] = {net: [], chain: chain, dir: dir} unless sets.key?(set)
                sets[set][:net].push(n)
            end

            sets.each do |k, v|
                cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{v[:chain]} "\
                    "#{proto} #{v[:dir]} { #{v[:net].join(', ')} } "\
                    "counter return comment \\\"#{k}\\\""
            end
        end

        # Implements the :net_portrange rule. Example:
        # nft add rule bridge filter one-3-0-i meta l4proto tcp \
        # ip saddr { 10.0.0.0/24 } tcp dport { 80 } \
        # counter return comment \"one-3-0-1-i-nr-ip\"
        def process_net_portrange(cmds, vars)
            the_nets = net()

            return if the_nets.empty?

            sets = {}

            the_nets.each do |n|
                if IPAddr.new(n).ipv6?
                    family = "ip6"
                else
                    family = "ip"
                end

                if @rule_type == :inbound
                    chain = vars[:chain_in]
                    set = "#{vars[:set_sg_in]}-nr-#{family}"
                    dir = "#{family} saddr"
                else
                    chain = vars[:chain_out]
                    set = "#{vars[:set_sg_out]}-nr-#{family}"
                    dir = "#{family} daddr"
                end

                sets[set] = {net: [], chain: chain, dir: dir} unless sets.key?(set)
                sets[set][:net].push(n)
            end

            range = @range.gsub(/:/, '-')

            sets.each do |k, v|
                cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{v[:chain]} "\
                    "meta l4proto #{@protocol} #{v[:dir]} { #{v[:net].join(', ')} } "\
                    "#{@protocol} dport { #{range} } "\
                    "counter return comment \\\"#{k}\\\""
            end
        end

        # Implements the :net_icmp_type rule. Example:
        # nft add rule bridge filter one-3-0-i meta l4proto icmp \
        # ip saddr { 10.0.0.0/24 } icmp type 8 \
        # icmp code { 0 } counter return \
        # comment \"one-3-0-1-i-ni\"
        def process_net_icmp_type(cmds, vars)
            if @rule_type == :inbound
                chain = vars[:chain_in]
                set = "#{vars[:set_sg_in]}-ni"
                dir = "ip saddr"
            else
                chain = vars[:chain_out]
                set = "#{vars[:set_sg_out]}-ni"
                dir = "ip daddr"
            end

            the_nets = net()
            return if the_nets.empty?

            codes = ICMP_TYPES_EXPANDED[@icmp_type.to_i]
            codes = "0" if codes.nil?

            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} "\
                "meta l4proto icmp #{dir} { #{the_nets.join(', ')} } "\
                "icmp type #{@icmp_type} icmp code { #{codes.join(', ')} } "\
                "counter return comment \\\"#{set}\\\""
        end

        # Implements the :net_icmpv6_type rule. Example:
        # nft add rule bridge filter one-3-0-i meta l4proto ipv6-icmp \
        # ip6 saddr { fd00::/64 } icmpv6 type 128 \
        # icmpv6 code { 0 } counter return \
        # comment \"one-3-0-1-i-ni6\"
        def process_net_icmpv6_type(cmds, vars)
            if @rule_type == :inbound
                chain = vars[:chain_in]
                set = "#{vars[:set_sg_in]}-ni6"
                dir = "ip6 saddr"
            else
                chain = vars[:chain_out]
                set = "#{vars[:set_sg_out]}-ni6"
                dir = "ip6 daddr"
            end

            the_nets = net()
            return if the_nets.empty?

            codes = ICMPv6_TYPES_EXPANDED[@icmpv6_type.to_i]
            codes = "0" if codes.nil?

            cmds.add :nft, "add rule bridge #{FILTER_TABLE} #{chain} "\
                "meta l4proto ipv6-icmp #{dir} { #{the_nets.join(', ')} } "\
                "icmpv6 type #{@icmpv6_type} icmpv6 code { #{codes.join(', ')} } "\
                "counter return comment \\\"#{set}\\\""
        end
    end

    ############################################################################
    # This class represents a SecurityGroup implemented with nft
    # Kernel facilities.
    ############################################################################
    class SecurityGroupNft < VNMNetwork::SecurityGroup
        def initialize(vm, nic, sg_id, rules)
            super

            @vars = SGNft.vars(@vm, @nic, @sg_id)
        end

        def new_rule(rule)
            RuleNft.new(rule)
        end
    end

    ############################################################################
    # Methods to configure the hypervisor nft rules. All the rules are
    # added to the FILTER_TABLE table. By default this chain is "filter"
    ############################################################################

    FILTER_TABLE = "filter"
    INPUT_CHAIN = "INPUT"
    FORWARD_CHAIN = "FORWARD"
    OUTPUT_CHAIN = "OUTPUT"

    # Get information from the current iptables rules and chains
    # @return [Hash] with the following keys:
    # - :forwards
    # - :chains
    def self.info
        commands = VNMNetwork::Commands.new

        chains = []
        forwards = []

        commands.add :nft, "list table bridge #{FILTER_TABLE} -a"
        table = commands.run!
        table.split("\n").each do |x| 
            x.match(/chain (\S+?) {/) { |m| chains.push(m[1]) }
            x.match(/ jump (\S+?) .*# handle (\d+)/) { |m| forwards.push([chains.last, m[1], m[2]]) }
            x.match(/set (\S+?) {/) { |m| sets.push(m[1]) }
        end

        {
            :forwards => forwards,
            :chains => chains,
        }
    end

    # Bootstrap the OpenNebula chains and rules. This method:
    # 1.- Creates the INPUT_CHAIN, FORWARD_CHAIN and OUTPUT_CHAIN chains
    # 2.- By default ACCEPT all traffic
    def self.global_bootstrap
        info = SGNft.info

        commands = VNMNetwork::Commands.new

        if !info[:chains].include?('INPUT')
            commands.add :nft, "add chain bridge #{FILTER_TABLE} #{INPUT_CHAIN} "\
                "{ type filter hook input priority -200\; policy accept\; }"
        end

        if !info[:chains].include?('FORWARD')
            commands.add :nft, "add chain bridge #{FILTER_TABLE} #{FORWARD_CHAIN} "\
                "{ type filter hook forward priority -200\; policy accept\; }"
        end

        if !info[:chains].include?('OUTPUT')
            commands.add :nft, "add chain bridge #{FILTER_TABLE} #{OUTPUT_CHAIN} "\
                "{ type filter hook output priority -200\; policy accept\; }"
        end

        commands.run! if !commands.empty?
    end

    # Returns the base chain and ipset names for the VM
    # @param vm [VM] the virtual machine
    # @param nic [Nic] of the VM
    # @param sg_id [Fixnum] ID of the SecurityGroup if any
    #
    # @return [Hash] with the :chain, :chain_in, :chain_out chain names, and
    # :set_sg_in and :set_seg_out ipset names.
    def self.vars(vm, nic, sg_id = nil)
        vm_id = vm['ID']
        nic_id = nic[:nic_id]

        vars = {}

        vars[:nic] = nic
        vars[:vm_id] = vm_id
        vars[:nic_id] = nic_id
        vars[:chain] = "one-#{vm_id}-#{nic_id}"
        vars[:chain_in] = "#{vars[:chain]}-i"
        vars[:chain_out] = "#{vars[:chain]}-o"

        if sg_id
            vars[:set_sg_in] = "#{vars[:chain]}-#{sg_id}-i"
            vars[:set_sg_out] = "#{vars[:chain]}-#{sg_id}-o"
        end

        vars
    end

    # Bootstrap NIC rules for the interface. It creates the :chain_in and
    # :chain_out and sets up FORWARD rules to these chains for inbound and
    # outbound traffic.
    #
    # This method also sets mac_spoofing, and ip_spoofing rules
    #
    # Example, for VM 3 and NIC 0
    # nft add chain bridge filter one-3-0-i
    # nft add chain bridge filter one-3-0-o
    #
    # nft insert rule bridge filter INPUT iifname "vnet0" jump one-3-0-o
    # nft insert rule bridge filter FORWARD iifname "vnet0" jump one-3-0-o
    # nft insert rule bridge filter FORWARD oifname "vnet0" jump one-3-0-i
    # nft insert rule bridge filter OUTPUT oifname "vnet0" jump one-3-0-i
    #
    # Mac spoofing (no output traffic from a different MAC)
    # nft add rule bridge filter one-3-0-o ether saddr != 02:00:00:00:00:01 counter drop
    #
    # IP spoofing
    # nft add rule bridge filter one-3-0-o ip saddr != { 10.0.0.1 } counter drop
    def self.nic_pre(vm, nic)
        commands = VNMNetwork::Commands.new

        vars = SGNft.vars(vm, nic)

        chain_in = vars[:chain_in]
        chain_out = vars[:chain_out]

        # create chains
        commands.add :nft, "add chain bridge #{FILTER_TABLE} #{chain_in}" # inbound
        commands.add :nft, "add chain bridge #{FILTER_TABLE} #{chain_out}" # outbound

        # Send traffic to the NIC chains
        commands.add :nft, "insert rule bridge #{FILTER_TABLE} #{INPUT_CHAIN} "\
            "iifname \"#{nic[:tap]}\" jump #{chain_out}"
        commands.add :nft, "insert rule bridge #{FILTER_TABLE} #{FORWARD_CHAIN} "\
            "iifname \"#{nic[:tap]}\" jump #{chain_out}"
        commands.add :nft, "insert rule bridge #{FILTER_TABLE} #{FORWARD_CHAIN} "\
            "oifname \"#{nic[:tap]}\" jump #{chain_in}"
        commands.add :nft, "insert rule bridge #{FILTER_TABLE} #{OUTPUT_CHAIN} "\
            "oifname \"#{nic[:tap]}\" jump #{chain_in}"

        # ARP for IPv4 (since we are on the bridge level)
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} ether type arp counter accept"
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} ether type arp counter accept"
        
        # ICMPv6 Neighbor Discovery Protocol (ARP replacement for IPv6)
        ## Allow routers to send router advertisements
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-router-advert counter accept"

        ## Allow neighbor solicitations to reach the host
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-neighbor-solicit counter accept"

        ## Allow neighbor solicitations replies to reach the host
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-neighbor-advert counter accept"

        ## Allow routers to send Redirect messages
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-redirect counter accept"

        ## Allow the host to send a router solicitation
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-router-solicit counter accept"

        ## Allow the host to send neighbor solicitation requests
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-neighbor-solicit counter accept"

        ## Allow the host to send neighbor solicitation replies
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} meta l4proto ipv6-icmp icmpv6 "\
            "type nd-neighbor-advert counter accept"

        # Mac-spofing
        if nic[:filter_mac_spoofing] == "YES"
            commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} "\
                "ether saddr != #{nic[:mac]} counter drop"
        end

        # IP-spofing
        if nic[:filter_ip_spoofing] == "YES"
            ipv4s = Array.new

            [:ip, :vrouter_ip].each do |key|
                ipv4s << nic[key] if !nic[key].nil? && !nic[key].empty?
            end

            if !ipv4s.empty?
                #bootp
                commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} "\
                    "ip daddr 255.255.255.255 udp sport 68 udp dport 67 "\
                    "counter return"

                commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} ip saddr != "\
                    "{ #{ipv4s.join(', ')} } counter drop"
            else # If there are no IPv4 addresses allowed, block all
                commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} counter "\
                    "drop"
            end

            ipv6s = Array.new

            [:ip6, :ip6_global, :ip6_link, :ip6_ula].each do |key|
                ipv6s << nic[key] if !nic[key].nil? && !nic[key].empty?
            end

            if !ipv6s.empty?
                commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} ip6 saddr != "\
                    "{ #{ipv6s.join(', ')} } counter drop"
            else # If there are no IPv6 addresses allowed, block all
                commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} counter drop"
            end
        end

        # ct is not supported by table bridge
        # Related, Established
        #commands.add :nft, "add rule ip #{FILTER_TABLE} #{chain_in}"\
        # " ct state related,established counter return"
        #commands.add :nft, "add rule ip #{FILTER_TABLE} #{chain_out}"\
        # " ct state related,established counter return"
        #commands.add :nft, "add rule ip6 #{FILTER_TABLE} #{chain_in}"\
        # " ct state related,established counter return"
        #commands.add :nft, "add rule ip6 #{FILTER_TABLE} #{chain_out}"\
        # " ct state related,established counter return"

        commands.run!
    end

    # Sets the default policy to DROP for the NIC rules. Example
    # nft add rule bridge filter one-3-0-i counter drop
    # nft add rule bridge filter one-3-0-o counter drop
    def self.nic_post(vm, nic)
        vars = SGNft.vars(vm, nic)
        chain_in = vars[:chain_in]
        chain_out = vars[:chain_out]

        commands = VNMNetwork::Commands.new

        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_in} counter drop"
        commands.add :nft, "add rule bridge #{FILTER_TABLE} #{chain_out} counter drop"

        commands.run!
    end

    # Removes all the rules associated to a VM and NIC
    def self.nic_deactivate(vm, nic)
        vars = SGNft.vars(vm, nic)
        chain = vars[:chain]
        chain_in = vars[:chain_in]
        chain_out = vars[:chain_out]

        info = self.info

        forwards = info[:forwards]
        chains = info[:chains]

        commands = VNMNetwork::Commands.new

        forwards.reverse_each do |fields|
            if [chain_in, chain_out].include?(fields[1])
                c = fields[0]
                n = fields[2]
                commands.add :nft, "delete rule bridge #{FILTER_TABLE} #{c} handle #{n}"
            end
        end

        remove_chains = chains.grep(/^#{chain}(-|$)/)
        remove_chains.each {|c| commands.add :nft, "flush chain bridge #{FILTER_TABLE} #{c}" }
        remove_chains.each {|c| commands.add :nft, "delete chain bridge #{FILTER_TABLE} #{c}" }

        commands.run!
    end
end

end

```

---

<div class="post-metadata">

### Author: ![ruben](https://yyz1.discourse-cdn.com/flex031/user_avatar/forum.opennebula.io/ruben/32/19_2.png) [@ruben](https://forum.opennebula.io/u/ruben)
#### Post date: [March 31, 2020, 3:50pm UTC](https://forum.opennebula.io/t/native-nft-security-groups/8273/3 "2020-03-31T15:50:42Z")

</div>

That’s awesome, Woud you mind to open a PR at [https://github.com/OpenNebula/one/pulls](https://github.com/OpenNebula/one/pulls)

In any case Thanks!!!

---

<div class="post-metadata">

### Author: ![Nico\_Schottelius](https://yyz1.discourse-cdn.com/flex031/user_avatar/forum.opennebula.io/nico_schottelius/32/6551_2.png) [@Nico\_Schottelius](https://forum.opennebula.io/u/Nico_Schottelius)
#### Post date: [May 17, 2020, 7:52pm UTC](https://forum.opennebula.io/t/native-nft-security-groups/8273/4 "2020-05-17T19:52:36Z")

</div>

Just joining the discussion here, as I created [https://github.com/OpenNebula/one/issues/4739](https://github.com/OpenNebula/one/issues/4739). Essentially it seems that OpenNebula’s security groups fail on Debian 10, because Debian 10 replaced ip(6)tables with nft.

Is there any public progress of above patch?
