From 90772fa9092b1899ebb6aec4832b8087966eb679 Mon Sep 17 00:00:00 2001 From: Mike Robski Date: Wed, 18 Sep 2024 11:38:07 +0300 Subject: [PATCH 1/4] doc/network_acl: ACL for bridge NIC device Support for ACLs for bridge NIC device when using nftables driver. Signed-off-by: Mike Robski --- doc/howto/network_acls.md | 3 +- doc/reference/devices_nic.md | 57 ++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/doc/howto/network_acls.md b/doc/howto/network_acls.md index c0994af8075..2a4c5774342 100644 --- a/doc/howto/network_acls.md +++ b/doc/howto/network_acls.md @@ -210,8 +210,9 @@ incus config device set security.acls.default.ingr When using network ACLs with a bridge network, be aware of the following limitations: - Unlike OVN ACLs, bridge ACLs are applied only on the boundary between the bridge and the Incus host. - This means they can only be used to apply network policies for traffic going to or from external networks. + This means they can only be used to apply network policies for traffic going to or from external networks (see exception for `nftables` firewall driver below). They cannot be used for to create {spellexception}`intra-bridge` firewalls, thus firewalls that control traffic between instances connected to the same bridge. +- When using the `nftables` firewall driver you can apply ACLs to the NIC device and control traffic between the instances. In this case the `reject` ACL rules are not permitted and when the default action is set to `reject` it is interpreted as `drop`. - {ref}`ACL groups and network selectors ` are not supported. - When using the `iptables` firewall driver, you cannot use IP range subjects (for example, `192.0.2.1-192.0.2.10`). - Baseline network service rules are added before ACL rules (in their respective INPUT/OUTPUT chains), because we cannot differentiate between INPUT/OUTPUT and FORWARD traffic once we have jumped into the ACL chain. diff --git a/doc/reference/devices_nic.md b/doc/reference/devices_nic.md index 452e0d42171..73c6400b1df 100644 --- a/doc/reference/devices_nic.md +++ b/doc/reference/devices_nic.md @@ -70,32 +70,37 @@ A `bridged` NIC uses an existing bridge on the host and creates a virtual device NIC devices of type `bridged` have the following device options: -Key | Type | Default | Managed | Description -:-- | :-- | :-- | :-- | :-- -`boot.priority` | integer | - | no | Boot priority for VMs (higher value boots first) -`host_name` | string | randomly assigned | no | The name of the interface inside the host -`hwaddr` | string | randomly assigned | no | The MAC address of the new interface -`ipv4.address` | string | - | no | An IPv4 address to assign to the instance through DHCP (can be `none` to restrict all IPv4 traffic when `security.ipv4_filtering` is set) -`ipv4.routes` | string | - | no | Comma-delimited list of IPv4 static routes to add on host to NIC -`ipv4.routes.external` | string | - | no | Comma-delimited list of IPv4 static routes to route to the NIC and publish on uplink network (BGP) -`ipv6.address` | string | - | no | An IPv6 address to assign to the instance through DHCP (can be `none` to restrict all IPv6 traffic when `security.ipv6_filtering` is set) -`ipv6.routes` | string | - | no | Comma-delimited list of IPv6 static routes to add on host to NIC -`ipv6.routes.external` | string | - | no | Comma-delimited list of IPv6 static routes to route to the NIC and publish on uplink network (BGP) -`limits.egress` | string | - | no | I/O limit in bit/s for outgoing traffic (various suffixes supported, see {ref}`instances-limit-units`) -`limits.ingress` | string | - | no | I/O limit in bit/s for incoming traffic (various suffixes supported, see {ref}`instances-limit-units`) -`limits.max` | string | - | no | I/O limit in bit/s for both incoming and outgoing traffic (same as setting both `limits.ingress` and `limits.egress`) -`limits.priority` | integer | - | no | The `skb->priority` value (32-bit unsigned integer) for outgoing traffic, to be used by the kernel queuing discipline (qdisc) to prioritize network packets (The effect of this value depends on the particular qdisc implementation, for example, `SKBPRIO` or `QFQ`. Consult the kernel qdisc documentation before setting this value.) -`mtu` | integer | parent MTU | yes | The MTU of the new interface -`name` | string | kernel assigned | no | The name of the interface inside the instance -`network` | string | - | no | The managed network to link the device to (instead of specifying the `nictype` directly) -`parent` | string | - | yes | The name of the host device (required if specifying the `nictype` directly) -`queue.tx.length` | integer | - | no | The transmit queue length for the NIC -`security.ipv4_filtering`| bool | `false` | no | Prevent the instance from spoofing another instance's IPv4 address (enables `security.mac_filtering`) -`security.ipv6_filtering`| bool | `false` | no | Prevent the instance from spoofing another instance's IPv6 address (enables `security.mac_filtering`) -`security.mac_filtering` | bool | `false` | no | Prevent the instance from spoofing another instance's MAC address -`security.port_isolation`| bool | `false` | no | Prevent the NIC from communicating with other NICs in the network that have port isolation enabled -`vlan` | integer | - | no | The VLAN ID to use for non-tagged traffic (can be `none` to remove port from default VLAN) -`vlan.tagged` | integer | - | no | Comma-delimited list of VLAN IDs or VLAN ranges to join for tagged traffic +Key | Type | Default | Managed | Description +:-- | :-- | :-- | :-- | :-- +`boot.priority` | integer | - | no | Boot priority for VMs (higher value boots first) +`host_name` | string | randomly assigned | no | The name of the interface inside the host +`hwaddr` | string | randomly assigned | no | The MAC address of the new interface +`ipv4.address` | string | - | no | An IPv4 address to assign to the instance through DHCP (can be `none` to restrict all IPv4 traffic when `security.ipv4_filtering` is set) +`ipv4.routes` | string | - | no | Comma-delimited list of IPv4 static routes to add on host to NIC +`ipv4.routes.external` | string | - | no | Comma-delimited list of IPv4 static routes to route to the NIC and publish on uplink network (BGP) +`ipv6.address` | string | - | no | An IPv6 address to assign to the instance through DHCP (can be `none` to restrict all IPv6 traffic when `security.ipv6_filtering` is set) +`ipv6.routes` | string | - | no | Comma-delimited list of IPv6 static routes to add on host to NIC +`ipv6.routes.external` | string | - | no | Comma-delimited list of IPv6 static routes to route to the NIC and publish on uplink network (BGP) +`limits.egress` | string | - | no | I/O limit in bit/s for outgoing traffic (various suffixes supported, see {ref}`instances-limit-units`) +`limits.ingress` | string | - | no | I/O limit in bit/s for incoming traffic (various suffixes supported, see {ref}`instances-limit-units`) +`limits.max` | string | - | no | I/O limit in bit/s for both incoming and outgoing traffic (same as setting both `limits.ingress` and `limits.egress`) +`limits.priority` | integer | - | no | The `skb->priority` value (32-bit unsigned integer) for outgoing traffic, to be used by the kernel queuing discipline (qdisc) to prioritize network packets (The effect of this value depends on the particular qdisc implementation, for example, `SKBPRIO` or `QFQ`. Consult the kernel qdisc documentation before setting this value.) +`mtu` | integer | parent MTU | yes | The MTU of the new interface +`name` | string | kernel assigned | no | The name of the interface inside the instance +`network` | string | - | no | The managed network to link the device to (instead of specifying the `nictype` directly) +`parent` | string | - | yes | The name of the host device (required if specifying the `nictype` directly) +`queue.tx.length` | integer | - | no | The transmit queue length for the NIC +`security.acls` | string | - | no | Comma-separated list of network ACLs to apply +`security.acls.default.egress.action` | string | `drop` | no | Action to use for egress traffic that doesn't match any ACL rule +`security.acls.default.egress.logged` | bool | `false` | no | Whether to log egress traffic that doesn't match any ACL rule +`security.acls.default.ingress.action`| string | `drop` | no | Action to use for ingress traffic that doesn't match any ACL rule +`security.acls.default.ingress.logged`| bool | `false` | no | Whether to log ingress traffic that doesn't match any ACL rule +`security.ipv4_filtering` | bool | `false` | no | Prevent the instance from spoofing another instance's IPv4 address (enables `security.mac_filtering`) +`security.ipv6_filtering` | bool | `false` | no | Prevent the instance from spoofing another instance's IPv6 address (enables `security.mac_filtering`) +`security.mac_filtering` | bool | `false` | no | Prevent the instance from spoofing another instance's MAC address +`security.port_isolation` | bool | `false` | no | Prevent the NIC from communicating with other NICs in the network that have port isolation enabled +`vlan` | integer | - | no | The VLAN ID to use for non-tagged traffic (can be `none` to remove port from default VLAN) +`vlan.tagged` | integer | - | no | Comma-delimited list of VLAN IDs or VLAN ranges to join for tagged traffic (nic-macvlan)= ### `nictype`: `macvlan` From eacae56e8faef7d6a4f91ef1d0b582abe0e215b2 Mon Sep 17 00:00:00 2001 From: Mike Robski Date: Wed, 18 Sep 2024 11:38:23 +0300 Subject: [PATCH 2/4] api: ACL support for bridge NIC device Support for security.acls* fields for bridge NIC device when using nftables driver. Signed-off-by: Mike Robski --- internal/server/device/nic_bridged.go | 47 +++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/internal/server/device/nic_bridged.go b/internal/server/device/nic_bridged.go index 562492a1117..9e0e42446f7 100644 --- a/internal/server/device/nic_bridged.go +++ b/internal/server/device/nic_bridged.go @@ -25,10 +25,13 @@ import ( deviceConfig "github.com/lxc/incus/v6/internal/server/device/config" "github.com/lxc/incus/v6/internal/server/dnsmasq" "github.com/lxc/incus/v6/internal/server/dnsmasq/dhcpalloc" + firewallDrivers "github.com/lxc/incus/v6/internal/server/firewall/drivers" "github.com/lxc/incus/v6/internal/server/instance" "github.com/lxc/incus/v6/internal/server/instance/instancetype" "github.com/lxc/incus/v6/internal/server/ip" "github.com/lxc/incus/v6/internal/server/network" + "github.com/lxc/incus/v6/internal/server/network/acl" + "github.com/lxc/incus/v6/internal/server/project" "github.com/lxc/incus/v6/internal/server/resources" localUtil "github.com/lxc/incus/v6/internal/server/util" internalUtil "github.com/lxc/incus/v6/internal/util" @@ -87,6 +90,11 @@ func (d *nicBridged) validateConfig(instConf instance.ConfigReader) error { "security.ipv4_filtering", "security.ipv6_filtering", "security.port_isolation", + "security.acls", + "security.acls.default.ingress.action", + "security.acls.default.egress.action", + "security.acls.default.ingress.logged", + "security.acls.default.egress.logged", "boot.priority", "vlan", } @@ -284,6 +292,24 @@ func (d *nicBridged) validateConfig(instConf instance.ConfigReader) error { } } + // Check if security ACL(s) are configured. + if d.config["security.acls"] != "" { + if d.state.Firewall.String() != "nftables" { + return fmt.Errorf("Security ACLs are only supported when using nftables firewall") + } + + // The NIC's network may be a non-default project, so lookup project and get network's project name. + networkProjectName, _, err := project.NetworkProject(d.state.DB.Cluster, instConf.Project().Name) + if err != nil { + return fmt.Errorf("Failed loading network project name: %w", err) + } + + err = acl.Exists(d.state, networkProjectName, util.SplitNTrimSpace(d.config["security.acls"], ",", -1, true)...) + if err != nil { + return err + } + } + rules := nicValidationRules(requiredFields, optionalFields, instConf) // Add bridge specific vlan validation. @@ -443,7 +469,7 @@ func (d *nicBridged) UpdatableFields(oldDevice Type) []string { return []string{} } - return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes", "ipv4.routes.external", "ipv6.routes.external", "ipv4.address", "ipv6.address", "security.mac_filtering", "security.ipv4_filtering", "security.ipv6_filtering"} + return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes", "ipv4.routes.external", "ipv6.routes.external", "ipv4.address", "ipv6.address", "security.mac_filtering", "security.ipv4_filtering", "security.ipv6_filtering", "security.acls", "security.acls.default.egress.action", "security.acls.default.egress.logged", "security.acls.default.ingress.action", "security.acls.default.ingress.logged"} } // Add is run when a device is added to a non-snapshot instance whether or not the instance is running. @@ -843,7 +869,7 @@ func (d *nicBridged) postStop() error { routes = append(routes, util.SplitNTrimSpace(d.config["ipv6.routes.external"], ",", -1, true)...) networkNICRouteDelete(d.config["parent"], routes...) - if util.IsTrue(d.config["security.mac_filtering"]) || util.IsTrue(d.config["security.ipv4_filtering"]) || util.IsTrue(d.config["security.ipv6_filtering"]) { + if util.IsTrue(d.config["security.mac_filtering"]) || util.IsTrue(d.config["security.ipv4_filtering"]) || util.IsTrue(d.config["security.ipv6_filtering"]) || d.config["security.acls"] != "" { d.removeFilters(d.config) } @@ -950,12 +976,12 @@ func (d *nicBridged) setupHostFilters(oldConfig deviceConfig.Device) (revert.Hoo } // Remove any old network filters if non-empty oldConfig supplied as part of update. - if oldConfig != nil && (util.IsTrue(oldConfig["security.mac_filtering"]) || util.IsTrue(oldConfig["security.ipv4_filtering"]) || util.IsTrue(oldConfig["security.ipv6_filtering"])) { + if oldConfig != nil && (util.IsTrue(oldConfig["security.mac_filtering"]) || util.IsTrue(oldConfig["security.ipv4_filtering"]) || util.IsTrue(oldConfig["security.ipv6_filtering"]) || oldConfig["security.acls"] != "") { d.removeFilters(oldConfig) } // Setup network filters. - if util.IsTrue(d.config["security.mac_filtering"]) || util.IsTrue(d.config["security.ipv4_filtering"]) || util.IsTrue(d.config["security.ipv6_filtering"]) { + if util.IsTrue(d.config["security.mac_filtering"]) || util.IsTrue(d.config["security.ipv4_filtering"]) || util.IsTrue(d.config["security.ipv6_filtering"]) || d.config["security.acls"] != "" { err := d.setFilters() if err != nil { return nil, err @@ -1045,7 +1071,7 @@ func (d *nicBridged) removeFilters(m deviceConfig.Device) { } // setFilters sets up any network level filters defined for the instance. -// These are controlled by the security.mac_filtering, security.ipv4_Filtering and security.ipv6_filtering config keys. +// These are controlled by the security.mac_filtering, security.ipv4_Filtering, security.ipv6_filtering and security.acls config keys. func (d *nicBridged) setFilters() (err error) { if d.config["hwaddr"] == "" { return fmt.Errorf("Failed to set network filters: require hwaddr defined") @@ -1135,7 +1161,16 @@ func (d *nicBridged) setFilters() (err error) { return err } - err = d.state.Firewall.InstanceSetupBridgeFilter(d.inst.Project().Name, d.inst.Name(), d.name, d.config["parent"], d.config["host_name"], d.config["hwaddr"], IPv4Nets, IPv6Nets, d.network != nil) + var aclRules []firewallDrivers.ACLRule + + if config["security.acls"] != "" { + aclRules, err = acl.FirewallACLRules(d.state, d.name, d.inst.Project().Name, d.config) + if err != nil { + return err + } + } + + err = d.state.Firewall.InstanceSetupBridgeFilter(d.inst.Project().Name, d.inst.Name(), d.name, d.config["parent"], d.config["host_name"], d.config["hwaddr"], IPv4Nets, IPv6Nets, d.network != nil, util.IsTrue(config["security.mac_filtering"]), aclRules) if err != nil { return err } From 654b1565e2324d01e8f001e70e3c329aae1075f1 Mon Sep 17 00:00:00 2001 From: Mike Robski Date: Wed, 18 Sep 2024 11:38:45 +0300 Subject: [PATCH 3/4] incusd/server/firewall: ACL for bridge NIC device Support for ACLs for bridge NIC device when using nftables driver. Modified nftable template to allow combining fitering and ACL rules. Updated ACL processing to detect bridge NIC devices with ACL applied and re-generate nftable if the instance is running. Signed-off-by: Mike Robski --- internal/server/device/config/devices.go | 3 + .../firewall/drivers/drivers_nftables.go | 172 +++++++++++++++++- .../drivers/drivers_nftables_templates.go | 93 +++++++--- .../firewall/drivers/drivers_xtables.go | 6 +- .../server/firewall/firewall_interface.go | 11 +- internal/server/network/acl/acl_bridge.go | 56 ++++++ internal/server/network/acl/acl_firewall.go | 26 ++- internal/server/network/acl/acl_load.go | 51 +++++- internal/server/network/acl/driver_common.go | 16 +- 9 files changed, 381 insertions(+), 53 deletions(-) create mode 100644 internal/server/network/acl/acl_bridge.go diff --git a/internal/server/device/config/devices.go b/internal/server/device/config/devices.go index 8f63254360f..eb5533c9c9c 100644 --- a/internal/server/device/config/devices.go +++ b/internal/server/device/config/devices.go @@ -168,6 +168,9 @@ func (list Devices) Update(newlist Devices, updateFields func(Device, Device) [] allChangedKeys := []string{} for key, d := range addlist { + // Remove the "force_update" key if present. + delete(d, "force_update") + srcOldDevice := rmlist[key] oldDevice := srcOldDevice.Clone() diff --git a/internal/server/firewall/drivers/drivers_nftables.go b/internal/server/firewall/drivers/drivers_nftables.go index e34fdfe0c5d..9bc4445f861 100644 --- a/internal/server/firewall/drivers/drivers_nftables.go +++ b/internal/server/firewall/drivers/drivers_nftables.go @@ -394,7 +394,7 @@ func (d Nftables) instanceDeviceLabel(projectName, instanceName, deviceName stri } // InstanceSetupBridgeFilter sets up the filter rules to apply bridged device IP filtering. -func (d Nftables) InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool) error { +func (d Nftables) InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool, macFiltering bool, aclRules []ACLRule) error { deviceLabel := d.instanceDeviceLabel(projectName, instanceName, deviceName) mac, err := net.ParseMAC(hwAddr) @@ -413,13 +413,19 @@ func (d Nftables) InstanceSetupBridgeFilter(projectName string, instanceName str "hwAddrHex": fmt.Sprintf("0x%s", hex.EncodeToString(mac)), } + if macFiltering { + tplFields["macFiltering"] = true + } + // Filter unwanted ethernet frames when using IP filtering. if len(IPv4Nets)+len(IPv6Nets) > 0 { tplFields["filterUnwantedFrames"] = true + tplFields["macFiltering"] = true } if IPv4Nets != nil && len(IPv4Nets) == 0 { tplFields["ipv4FilterAll"] = true + tplFields["macFiltering"] = true } ipv4Nets := make([]string, 0, len(IPv4Nets)) @@ -429,9 +435,11 @@ func (d Nftables) InstanceSetupBridgeFilter(projectName string, instanceName str if IPv6Nets != nil && len(IPv6Nets) == 0 { tplFields["ipv6FilterAll"] = true + tplFields["macFiltering"] = true } - ipv6Nets := make([]map[string]string, 0, len(IPv6Nets)) + ipv6NetsList := make([]string, 0, len(IPv6Nets)) + ipv6NetsPrefixList := make([]string, 0, len(IPv6Nets)) for _, ipv6Net := range IPv6Nets { ones, _ := ipv6Net.Mask.Size() prefix, err := subnetPrefixHex(ipv6Net) @@ -439,15 +447,28 @@ func (d Nftables) InstanceSetupBridgeFilter(projectName string, instanceName str return err } - ipv6Nets = append(ipv6Nets, map[string]string{ - "net": ipv6Net.String(), - "nBits": strconv.Itoa(ones), - "hexPrefix": fmt.Sprintf("0x%s", prefix), - }) + ipv6NetsList = append(ipv6NetsList, ipv6Net.String()) + ipv6NetsPrefixList = append(ipv6NetsPrefixList, fmt.Sprintf("@nh,384,%d != 0x%s", ones, prefix)) + } + + tplFields["ipv4NetsList"] = strings.Join(ipv4Nets, ", ") + tplFields["ipv6NetsList"] = strings.Join(ipv6NetsList, ", ") + tplFields["ipv6NetsPrefixList"] = strings.Join(ipv6NetsPrefixList, " ") + + // Process the assigned ACL rules and convert them to NFT rules + nftRules, err := d.aclRulesToNftRules(hostName, aclRules) + if err != nil { + return fmt.Errorf("Failed generating bridge ACL rules for instance device %q (%s): %w", deviceLabel, tplFields["family"], err) } - tplFields["ipv4Nets"] = ipv4Nets - tplFields["ipv6Nets"] = ipv6Nets + // Set the template fields for the ACL rules. + tplFields["aclInDropRules"] = nftRules.inDropRules + tplFields["aclInAcceptRules"] = append(nftRules.inAcceptRules4, nftRules.inAcceptRules6...) + tplFields["aclInDefaultRule"] = nftRules.defaultInRule + + tplFields["aclOutDropRules"] = nftRules.outDropRules + tplFields["aclOutAcceptRules"] = nftRules.outAcceptRules + tplFields["aclOutDefaultRule"] = nftRules.defaultOutRule err = d.applyNftConfig(nftablesInstanceBridgeFilter, tplFields) if err != nil { @@ -462,7 +483,7 @@ func (d Nftables) InstanceClearBridgeFilter(projectName string, instanceName str deviceLabel := d.instanceDeviceLabel(projectName, instanceName, deviceName) // Remove chains created by bridge filter rules. - err := d.removeChains([]string{"bridge"}, deviceLabel, "in", "fwd") + err := d.removeChains([]string{"bridge"}, deviceLabel, "in", "fwd", "out") if err != nil { return fmt.Errorf("Failed clearing bridge filter rules for instance device %q: %w", deviceLabel, err) } @@ -573,6 +594,137 @@ func (d Nftables) InstanceClearProxyNAT(projectName string, instanceName string, return nil } +// nftRulesCollection contains the ACL rules translated to NFT rules and split in groups. +type nftRulesCollection struct { + inDropRules []string + inAcceptRules4 []string + inAcceptRules6 []string + outDropRules []string + outAcceptRules []string + defaultInRule string + defaultOutRule string +} + +// aclRulesToNftRules converts ACL rules applied to the device to NFT rules. +func (d Nftables) aclRulesToNftRules(hostName string, aclRules []ACLRule) (*nftRulesCollection, error) { + nftRules := nftRulesCollection{ + inDropRules: make([]string, 0), + inAcceptRules4: make([]string, 0), + inAcceptRules6: make([]string, 0), + outDropRules: make([]string, 0), + outAcceptRules: make([]string, 0), + defaultInRule: "", + defaultOutRule: "", + } + + hostNameQuoted := "\"" + hostName + "\"" + rulesCount := len(aclRules) + + for i, rule := range aclRules { + if i >= rulesCount-2 { + // The last two rules are the default ACL rules and we should keep them separate. + if rule.Action == "reject" { + // Reject is not supported in bridge filter and is converted to drop. + rule.Action = "drop" + } + + var partial bool + var err error + if rule.Direction == "egress" { + nftRules.defaultInRule, partial, err = d.aclRuleCriteriaToRules(hostNameQuoted, 4, &rule) + } else { + nftRules.defaultOutRule, partial, err = d.aclRuleCriteriaToRules(hostNameQuoted, 4, &rule) + } + + if err != nil { + return nil, err + } + + if partial { + return nil, fmt.Errorf("Invalid default rule generated") + } + + continue + } + + nft4Rule := "" + nft6Rule := "" + + // First try generating rules with IPv4 or IP agnostic criteria. + nft4Rule, partial, err := d.aclRuleCriteriaToRules(hostNameQuoted, 4, &rule) + if err != nil { + return nil, err + } + + if partial { + // If we couldn't fully generate the ruleset with only IPv4 or IP agnostic criteria, then + // fill in the remaining parts using IPv6 criteria. + nft6Rule, _, err = d.aclRuleCriteriaToRules(hostNameQuoted, 6, &rule) + if err != nil { + return nil, err + } + + if nft6Rule == "" { + return nil, fmt.Errorf("Invalid empty rule generated") + } + } else if nft4Rule == "" { + return nil, fmt.Errorf("Invalid empty rule generated") + } + + newNftRules := []string{} + if nft4Rule != "" { + newNftRules = append(newNftRules, nft4Rule) + } + + if nft6Rule != "" { + newNftRules = append(newNftRules, nft6Rule) + } + + switch rule.Direction { + case "ingress": + switch { + case rule.Action == "drop": + nftRules.outDropRules = append(nftRules.outDropRules, newNftRules...) + + case rule.Action == "reject": + return nil, fmt.Errorf("Invalid action %q for bridge filter", rule.Action) + + case rule.Action == "allow": + nftRules.outAcceptRules = append(nftRules.outAcceptRules, newNftRules...) + + default: + return nil, fmt.Errorf("Unrecognised action %q", rule.Action) + } + + case "egress": + switch { + case rule.Action == "drop": + nftRules.inDropRules = append(nftRules.inDropRules, newNftRules...) + + case rule.Action == "reject": + return nil, fmt.Errorf("Invalid action %q for bridge filter", rule.Action) + + case rule.Action == "allow": + if nft4Rule != "" { + nftRules.inAcceptRules4 = append(nftRules.inAcceptRules4, nft4Rule) + } + + if nft6Rule != "" { + nftRules.inAcceptRules6 = append(nftRules.inAcceptRules6, nft6Rule) + } + + default: + return nil, fmt.Errorf("Unrecognised action %q", rule.Action) + } + + default: + return nil, fmt.Errorf("Unrecognised direction %q", rule.Direction) + } + } + + return &nftRules, nil +} + // applyNftConfig loads the specified config template and then applies it to the common template before sending to // the nft command to be atomically applied to the system. func (d Nftables) applyNftConfig(tpl *template.Template, tplFields map[string]any) error { diff --git a/internal/server/firewall/drivers/drivers_nftables_templates.go b/internal/server/firewall/drivers/drivers_nftables_templates.go index ecc1d76c87b..88e506468d5 100644 --- a/internal/server/firewall/drivers/drivers_nftables_templates.go +++ b/internal/server/firewall/drivers/drivers_nftables_templates.go @@ -185,72 +185,113 @@ table {{.family}} {{.namespace}} { var nftablesInstanceBridgeFilter = template.Must(template.New("nftablesInstanceBridgeFilter").Parse(` chain in{{.chainSeparator}}{{.deviceLabel}} { type filter hook input priority -200; policy accept; + {{if .macFiltering -}} iifname "{{.hostName}}" ether saddr != {{.hwAddr}} drop iifname "{{.hostName}}" ether type arp arp saddr ether != {{.hwAddr}} drop iifname "{{.hostName}}" ether type ip6 icmpv6 type 136 @nh,528,48 != {{.hwAddrHex}} drop - {{if .ipv4Nets -}} + {{- end}} + {{if .ipv4NetsList -}} iifname "{{.hostName}}" ether type ip ip saddr 0.0.0.0 ip daddr 255.255.255.255 udp dport 67 accept - {{range .ipv4Nets -}} - iifname "{{$.hostName}}" ether type arp arp saddr ip {{.}} accept - iifname "{{$.hostName}}" ether type ip ip saddr {{.}} accept - {{end}} - iifname "{{.hostName}}" ether type arp drop - iifname "{{.hostName}}" ether type ip drop + iifname "{{.hostName}}" ether type arp arp saddr ip != { {{.ipv4NetsList}} } drop + iifname "{{.hostName}}" ether type ip ip saddr != { {{.ipv4NetsList}} } drop {{- end}} {{if .ipv4FilterAll -}} iifname "{{.hostName}}" ether type arp drop iifname "{{.hostName}}" ether type ip drop {{- end}} - {{if .ipv6Nets -}} + {{if .ipv6NetsList -}} iifname "{{.hostName}}" ether type ip6 ip6 saddr fe80::/10 ip6 daddr ff02::1:2 udp dport 547 accept iifname "{{.hostName}}" ether type ip6 ip6 saddr fe80::/10 ip6 daddr ff02::2 icmpv6 type 133 accept iifname "{{.hostName}}" ether type ip6 icmpv6 type 134 drop - {{ range .ipv6Nets -}} - iifname "{{$.hostName}}" ether type ip6 icmpv6 type 136 @nh,384,{{.nBits}} {{.hexPrefix}} accept - iifname "{{$.hostName}}" ether type ip6 ip6 saddr {{.net}} accept - {{end}} - iifname "{{.hostName}}" ether type ip6 drop + iifname "{{.hostName}}" ether type ip6 icmpv6 type 136 {{.ipv6NetsPrefixList}} drop + iifname "{{.hostName}}" ether type ip6 ip6 saddr != { {{.ipv6NetsList}} } drop {{- end}} {{if .ipv6FilterAll -}} iifname "{{.hostName}}" ether type ip6 drop {{- end}} + {{- if .aclInChain -}} + ct state established,related accept + {{- end}} + {{- range .aclInDropRules}} + {{.}} + {{- end}} + {{- range .aclInAcceptRules}} + {{.}} + {{- end}} {{if .filterUnwantedFrames -}} iifname "{{.hostName}}" ether type != {arp, ip, ip6} drop {{- end}} + {{if or .aclInDropRules .aclInAcceptRules -}} + iifname "{{.hostName}}" ether type arp accept + iifname "{{.hostName}}" ip6 nexthdr ipv6-icmp icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } accept + {{- end}} + {{.aclInDefaultRule}} } chain fwd{{.chainSeparator}}{{.deviceLabel}} { type filter hook forward priority -200; policy accept; + {{if .macFiltering -}} iifname "{{.hostName}}" ether saddr != {{.hwAddr}} drop iifname "{{.hostName}}" ether type arp arp saddr ether != {{.hwAddr}} drop iifname "{{.hostName}}" ether type ip6 icmpv6 type 136 @nh,528,48 != {{.hwAddrHex}} drop - {{if .ipv4Nets -}} - {{range .ipv4Nets -}} - iifname "{{$.hostName}}" ether type arp arp saddr ip {{.}} accept - iifname "{{$.hostName}}" ether type ip ip saddr {{.}} accept - {{end}} - iifname "{{.hostName}}" ether type arp drop - iifname "{{.hostName}}" ether type ip drop + {{- end}} + {{if .ipv4NetsList -}} + iifname "{{.hostName}}" ether type arp arp saddr ip != { {{.ipv4NetsList}} } drop + iifname "{{.hostName}}" ether type ip ip saddr != { {{.ipv4NetsList}} } drop {{end}} {{if .ipv4FilterAll -}} iifname "{{.hostName}}" ether type arp drop iifname "{{.hostName}}" ether type ip drop {{- end}} - {{if .ipv6Nets -}} + {{if .ipv6NetsList -}} iifname "{{.hostName}}" ether type ip6 icmpv6 type 134 drop - {{range .ipv6Nets}} - iifname "{{$.hostName}}" ether type ip6 ip6 saddr {{.net}} accept - iifname "{{$.hostName}}" ether type ip6 icmpv6 type 136 @nh,384,{{.nBits}} {{.hexPrefix}} accept - {{end}} - iifname "{{.hostName}}" ether type ip6 drop + iifname "{{.hostName}}" ether type ip6 icmpv6 type 136 {{.ipv6NetsPrefixList}} drop + iifname "{{.hostName}}" ether type ip6 ip6 saddr != { {{.ipv6NetsList}} } drop {{- end}} {{if .ipv6FilterAll -}} iifname "{{.hostName}}" ether type ip6 drop {{- end}} + {{- range .aclInDropRules}} + {{.}} + {{- end}} + {{- range .aclOutDropRules}} + {{.}} + {{- end}} + {{- range .aclInAcceptRules}} + {{.}} + {{- end}} + {{- range .aclOutAcceptRules}} + {{.}} + {{- end}} {{if .filterUnwantedFrames -}} iifname "{{.hostName}}" ether type != {arp, ip, ip6} drop {{- end}} + {{if or .aclInDropRules .aclInAcceptRules -}} + iifname "{{.hostName}}" ether type arp accept + iifname "{{.hostName}}" ip6 nexthdr ipv6-icmp icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } accept + {{- end}} + {{if or .aclOutDropRules .aclOutAcceptRules -}} + oifname "{{.hostName}}" ether type arp accept + oifname "{{.hostName}}" ip6 nexthdr ipv6-icmp icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } accept + {{- end}} + {{.aclInDefaultRule}} + {{.aclOutDefaultRule}} +} + +{{if or .aclOutDropRules .aclOutAcceptRules -}} +chain out{{.chainSeparator}}{{.deviceLabel}} { + type filter hook output priority filter; policy accept; + {{- range .aclOutDropRules}} + {{.}} + {{- end}} + {{- range .aclOutAcceptRules}} + {{.}} + {{- end}} + oifname "{{.hostName}}" ether type arp accept + oifname "{{.hostName}}" ip6 nexthdr ipv6-icmp icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } accept + {{.aclOutDefaultRule}} } +{{- end}} `)) // nftablesInstanceRPFilter defines the rules to perform reverse path filtering. diff --git a/internal/server/firewall/drivers/drivers_xtables.go b/internal/server/firewall/drivers/drivers_xtables.go index 8d969b9ea2d..4d3df32b946 100644 --- a/internal/server/firewall/drivers/drivers_xtables.go +++ b/internal/server/firewall/drivers/drivers_xtables.go @@ -810,7 +810,11 @@ func (d Xtables) instanceDeviceIPTablesComment(projectName string, instanceName // If the parent bridge is managed by Incus then parentManaged argument should be true so that the rules added can // use the iptablesChainACLFilterPrefix chain. If not they are added to the main filter chains directly (which only // works for unmanaged bridges because those don't support ACLs). -func (d Xtables) InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool) error { +func (d Xtables) InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool, macFiltering bool, aclRules []ACLRule) error { + if len(aclRules) > 0 { + return fmt.Errorf("ACL rules not supported for xtables bridge filtering") + } + comment := d.instanceDeviceIPTablesComment(projectName, instanceName, deviceName) rules := d.generateFilterEbtablesRules(hostName, hwAddr, IPv4Nets, IPv6Nets) diff --git a/internal/server/firewall/firewall_interface.go b/internal/server/firewall/firewall_interface.go index 1d6c0e160ba..c07b519a33e 100644 --- a/internal/server/firewall/firewall_interface.go +++ b/internal/server/firewall/firewall_interface.go @@ -6,6 +6,15 @@ import ( "github.com/lxc/incus/v6/internal/server/firewall/drivers" ) +// FirewallRules represents a set of firewall rules. +type FirewallRules struct { + Rules []drivers.ACLRule + IngressAction string + IngressLogged bool + EgressAction string + EgressLogged bool +} + // Firewall represents an Incus firewall. type Firewall interface { String() string @@ -16,7 +25,7 @@ type Firewall interface { NetworkApplyACLRules(networkName string, rules []drivers.ACLRule) error NetworkApplyForwards(networkName string, rules []drivers.AddressForward) error - InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool) error + InstanceSetupBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet, parentManaged bool, macFiltering bool, aclRules []drivers.ACLRule) error InstanceClearBridgeFilter(projectName string, instanceName string, deviceName string, parentName string, hostName string, hwAddr string, IPv4Nets []*net.IPNet, IPv6Nets []*net.IPNet) error InstanceSetupProxyNAT(projectName string, instanceName string, deviceName string, forward *drivers.AddressForward) error diff --git a/internal/server/network/acl/acl_bridge.go b/internal/server/network/acl/acl_bridge.go new file mode 100644 index 00000000000..af36781aea4 --- /dev/null +++ b/internal/server/network/acl/acl_bridge.go @@ -0,0 +1,56 @@ +package acl + +import ( + "github.com/lxc/incus/v6/internal/server/db" + deviceConfig "github.com/lxc/incus/v6/internal/server/device/config" + "github.com/lxc/incus/v6/internal/server/instance" + "github.com/lxc/incus/v6/internal/server/state" + "github.com/lxc/incus/v6/shared/logger" +) + +// BridgeUpdateACLs forces the update of all NIC devices who have the changed ACL applied. +func BridgeUpdateACLs(s *state.State, l logger.Logger, aclProjectName string, aclNetDevices map[string]NetworkACLUsage) error { + // Update of the bridge NICs affected by the ACL change + for _, aclNetDevice := range aclNetDevices { + inst, err := instance.LoadByProjectAndName(s, aclProjectName, aclNetDevice.InstanceName) + if err != nil { + return err + } + + // Skip stopped instances + if !inst.IsRunning() { + continue + } + + devices := inst.LocalDevices().CloneNative() + + // Test if device named aclNetDevice.DeviceName is present in the instance + _, found := devices[aclNetDevice.DeviceName] + + if !found { + continue + } + + l.Debug("Forcing update to NIC device to update ACL rules", logger.Ctx{"instance": aclNetDevice.InstanceName, "nicDevice": aclNetDevice.DeviceName}) + + // Set the "force_update" key to force the device to be updated due to difference in the config + devices[aclNetDevice.DeviceName]["force_update"] = "true" + + args := db.InstanceArgs{ + Architecture: inst.Architecture(), + Config: inst.ExpandedConfig(), + Description: inst.Description(), + Devices: deviceConfig.NewDevices(devices), + Ephemeral: inst.IsEphemeral(), + Profiles: inst.Profiles(), + Project: inst.Project().Name, + } + + err = inst.Update(args, false) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/server/network/acl/acl_firewall.go b/internal/server/network/acl/acl_firewall.go index fc6166660aa..f9ea971ccec 100644 --- a/internal/server/network/acl/acl_firewall.go +++ b/internal/server/network/acl/acl_firewall.go @@ -14,6 +14,16 @@ import ( // FirewallApplyACLRules applies ACL rules to network firewall. func FirewallApplyACLRules(s *state.State, logger logger.Logger, aclProjectName string, aclNet NetworkACLUsage) error { + rules, err := FirewallACLRules(s, aclNet.Name, aclProjectName, aclNet.Config) + if err != nil { + return err + } + + return s.Firewall.NetworkApplyACLRules(aclNet.Name, rules) +} + +// FirewallACLRules returns ACL rules for network firewall. +func FirewallACLRules(s *state.State, aclDeviceName string, aclProjectName string, config map[string]string) ([]firewallDrivers.ACLRule, error) { var dropRules []firewallDrivers.ACLRule var rejectRules []firewallDrivers.ACLRule var allowRules []firewallDrivers.ACLRule @@ -61,10 +71,10 @@ func FirewallApplyACLRules(s *state.State, logger logger.Logger, aclProjectName return nil } - logPrefix := aclNet.Name + logPrefix := aclDeviceName // Load ACLs specified by network. - for _, aclName := range util.SplitNTrimSpace(aclNet.Config["security.acls"], ",", -1, true) { + for _, aclName := range util.SplitNTrimSpace(config["security.acls"], ",", -1, true) { var aclInfo *api.NetworkACL err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { @@ -75,17 +85,17 @@ func FirewallApplyACLRules(s *state.State, logger logger.Logger, aclProjectName return err }) if err != nil { - return fmt.Errorf("Failed loading ACL %q for network %q: %w", aclName, aclNet.Name, err) + return nil, fmt.Errorf("Failed loading ACL %q for network %q: %w", aclName, aclDeviceName, err) } err = convertACLRules("ingress", logPrefix, aclInfo.Ingress...) if err != nil { - return fmt.Errorf("Failed converting ACL %q ingress rules for network %q: %w", aclInfo.Name, aclNet.Name, err) + return nil, fmt.Errorf("Failed converting ACL %q ingress rules for network %q: %w", aclInfo.Name, aclDeviceName, err) } err = convertACLRules("egress", logPrefix, aclInfo.Egress...) if err != nil { - return fmt.Errorf("Failed converting ACL %q egress rules for network %q: %w", aclInfo.Name, aclNet.Name, err) + return nil, fmt.Errorf("Failed converting ACL %q egress rules for network %q: %w", aclInfo.Name, aclDeviceName, err) } } @@ -96,8 +106,8 @@ func FirewallApplyACLRules(s *state.State, logger logger.Logger, aclProjectName rules = append(rules, allowStatelessRules...) // Add the automatic default ACL rule for the network. - egressAction, egressLogged := firewallACLDefaults(aclNet.Config, "egress") - ingressAction, ingressLogged := firewallACLDefaults(aclNet.Config, "ingress") + egressAction, egressLogged := firewallACLDefaults(config, "egress") + ingressAction, ingressLogged := firewallACLDefaults(config, "ingress") rules = append(rules, firewallDrivers.ACLRule{ Direction: "egress", @@ -113,7 +123,7 @@ func FirewallApplyACLRules(s *state.State, logger logger.Logger, aclProjectName LogName: fmt.Sprintf("%s-ingress", logPrefix), }) - return s.Firewall.NetworkApplyACLRules(aclNet.Name, rules) + return rules, nil } // firewallACLDefaults returns the action and logging mode to use for the specified direction's default rule. diff --git a/internal/server/network/acl/acl_load.go b/internal/server/network/acl/acl_load.go index 4cd04845c04..6ccef490eb5 100644 --- a/internal/server/network/acl/acl_load.go +++ b/internal/server/network/acl/acl_load.go @@ -302,10 +302,12 @@ func isInUseByDevice(d deviceConfig.Device, matchACLNames ...string) []string { // NetworkACLUsage info about a network and what ACL it uses. type NetworkACLUsage struct { - ID int64 - Name string - Type string - Config map[string]string + ID int64 + Name string + Type string + Config map[string]string + InstanceName string + DeviceName string } // NetworkUsage populates the provided aclNets map with networks that are using any of the specified ACLs. @@ -313,9 +315,9 @@ func NetworkUsage(s *state.State, aclProjectName string, aclNames []string, aclN supportedNetTypes := []string{"bridge", "ovn"} // Find all networks and instance/profile NICs that use any of the specified Network ACLs. - err := UsedBy(s, aclProjectName, func(ctx context.Context, tx *db.ClusterTx, matchedACLNames []string, usageType any, _ string, nicConfig map[string]string) error { + err := UsedBy(s, aclProjectName, func(ctx context.Context, tx *db.ClusterTx, matchedACLNames []string, usageType any, devName string, nicConfig map[string]string) error { switch u := usageType.(type) { - case db.InstanceArgs, cluster.Profile: + case cluster.Profile: networkID, network, _, err := tx.GetNetworkInAnyState(ctx, aclProjectName, nicConfig["network"]) if err != nil { return fmt.Errorf("Failed to load network %q: %w", nicConfig["network"], err) @@ -333,6 +335,43 @@ func NetworkUsage(s *state.State, aclProjectName string, aclNames []string, aclN } } + case db.InstanceArgs: + networkID, network, _, err := tx.GetNetworkInAnyState(ctx, aclProjectName, nicConfig["network"]) + if err != nil { + return fmt.Errorf("Failed to load network %q: %w", nicConfig["network"], err) + } + + if slices.Contains(supportedNetTypes, network.Type) { + if network.Type == "bridge" && devName != "" { + // Use different key for the usage by bridge NICs to avoid overwriting the usage by the bridge network itself. + key := fmt.Sprintf("%s/%s/%s", network.Name, u.Name, devName) + + _, found := aclNets[key] + + if !found { + aclNets[key] = NetworkACLUsage{ + ID: networkID, + Name: network.Name, + Type: network.Type, + Config: network.Config, + InstanceName: u.Name, + DeviceName: devName, + } + } + } else { + _, found := aclNets[network.Name] + + if !found { + aclNets[network.Name] = NetworkACLUsage{ + ID: networkID, + Name: network.Name, + Type: network.Type, + Config: network.Config, + } + } + } + } + case *api.Network: if slices.Contains(supportedNetTypes, u.Type) { _, found := aclNets[u.Name] diff --git a/internal/server/network/acl/driver_common.go b/internal/server/network/acl/driver_common.go index ebbbbc29402..f1c4a79fcd8 100644 --- a/internal/server/network/acl/driver_common.go +++ b/internal/server/network/acl/driver_common.go @@ -11,7 +11,7 @@ import ( "strings" "sync" - "github.com/lxc/incus/v6/client" + incus "github.com/lxc/incus/v6/client" internalInstance "github.com/lxc/incus/v6/internal/instance" "github.com/lxc/incus/v6/internal/revert" "github.com/lxc/incus/v6/internal/server/cluster" @@ -626,11 +626,17 @@ func (d *common) Update(config *api.NetworkACLPut, clientType request.ClientType // Separate out OVN networks from non-OVN networks. This is because OVN networks share ACL config, and // so changes are not applied entirely on a per-network basis and need to be treated differently. + // Separate the bridge networks used indirectly by NIC devices. This is because the ACL rules need to be + // applied to the bridge interface, not the network. aclOVNNets := map[string]NetworkACLUsage{} + aclBridgeNICs := map[string]NetworkACLUsage{} for k, v := range aclNets { if v.Type == "ovn" { delete(aclNets, k) aclOVNNets[k] = v + } else if v.Type == "bridge" && v.DeviceName != "" { + delete(aclNets, k) + aclBridgeNICs[k] = v } else if v.Type != "bridge" { return fmt.Errorf("Unsupported network ACL type %q", v.Type) } @@ -644,6 +650,14 @@ func (d *common) Update(config *api.NetworkACLPut, clientType request.ClientType } } + // If there are affected bridge NICs, apply the ACL changes to the bridge interface filter. + if len(aclBridgeNICs) > 0 { + err := BridgeUpdateACLs(d.state, d.logger, d.projectName, aclBridgeNICs) + if err != nil { + return fmt.Errorf("Failed updating bridge NIC ACL: %w", err) + } + } + // If there are affected OVN networks, then apply the changes, but only if the request type is normal. // This way we won't apply the same changes multiple times for each cluster member. if len(aclOVNNets) > 0 && clientType == request.ClientTypeNormal { From ea2bcd28df716a865a6bef7c7b35db6872722abb Mon Sep 17 00:00:00 2001 From: Mike Robski Date: Wed, 18 Sep 2024 11:39:02 +0300 Subject: [PATCH 4/4] tests: nftable test with ACL rules Support for ACLs for bridge NIC device when using nftables driver. Signed-off-by: Mike Robski --- ...container_devices_nic_bridged_filtering.sh | 44 +++---------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/test/suites/container_devices_nic_bridged_filtering.sh b/test/suites/container_devices_nic_bridged_filtering.sh index ca87f69c26b..cd61eb3b9f8 100644 --- a/test/suites/container_devices_nic_bridged_filtering.sh +++ b/test/suites/container_devices_nic_bridged_filtering.sh @@ -179,27 +179,11 @@ test_container_devices_nic_bridged_filtering() { echo "MAC ARP filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" ip saddr 192.0.2.2 accept"; then + if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" ip saddr != { 192.0.2.2, 198.51.100.0/24, 203.0.113.0/24 } drop"; then echo "IPv4 filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" arp saddr ip 192.0.2.2 accept"; then - echo "IPv4 ARP filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" ip saddr 198.51.100.0/24 accept"; then - echo "IPv4 filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" arp saddr ip 198.51.100.0/24 accept"; then - echo "IPv4 ARP filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" ip saddr 203.0.113.0/24 accept"; then - echo "IPv4 filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" arp saddr ip 203.0.113.0/24 accept"; then + if ! nft -nn list chain bridge incus "${table}.${ctPrefix}A.eth0" | grep -e "iifname \"${ctAHost}\" arp saddr ip != { 192.0.2.2, 198.51.100.0/24, 203.0.113.0/24 } drop"; then echo "IPv4 ARP filter not applied as part of ipv4_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi @@ -422,27 +406,11 @@ test_container_devices_nic_bridged_filtering() { echo "MAC NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,128 (${ipv6Hex}|${ipv6Dec}) accept"; then - echo "IPv6 NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr 2001:db8:1::2 accept"; then - echo "IPv6 filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,64 (${ipv6RoutesHex}|${ipv6RoutesDec}) accept"; then - echo "IPv6 NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr 2001:db8:2::/64 accept"; then - echo "IPv6 filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" - false - fi - if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,64 (${ipv6RoutesExternalHex}|${ipv6RoutesExternalDec}) accept"; then + if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,128 != (${ipv6Hex}|${ipv6Dec}) @nh,384,64 != (${ipv6RoutesHex}|${ipv6RoutesDec}) @nh,384,64 != (${ipv6RoutesExternalHex}|${ipv6RoutesExternalDec}) drop"; then echo "IPv6 NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr 2001:db8:3::/64 accept"; then + if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr != { 2001:db8:1::2, 2001:db8:2::/64, 2001:db8:3::/64 } drop"; then echo "IPv6 filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi @@ -694,11 +662,11 @@ test_container_devices_nic_bridged_filtering() { echo "MAC NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,128 (${ipv6Hex}|${ipv6Dec}) accept"; then + if ! echo "${rules}" | grep -P "iifname \"${ctAHost}\" icmpv6 type 136 @nh,384,128 != (${ipv6Hex}|${ipv6Dec}) drop"; then echo "IPv6 NDP filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi - if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr 2001:db8::2 accept"; then + if ! echo "${rules}" | grep "iifname \"${ctAHost}\" ip6 saddr != 2001:db8::2 drop"; then echo "IPv6 filter not applied as part of ipv6_filtering in nftables (${table}.${ctPrefix}A.eth0)" false fi