[ovs fw] Restrict IPv6 NA and DHCP(v6) IP and MAC source addresses

Neighbor Advertisments are used to inform other machines of the MAC
address to use to reach an IPv6. This commits prevents VMs from
pretending they are assigned IPv6 they should not use.

It also prevents sending UDP packets with spoofed IP or MAC even using
DHCP(v6) request ports.

Co-authored-by: David Sinquin <david.sinquin@gandi.net>

Closes-bug: #1902917

Conflicts:
    neutron/agent/linux/openvswitch_firewall/firewall.py

Change-Id: Iffb6643359562487414460f5a7e19a7fae9f935c
(cherry picked from commit ca7822e210)
This commit is contained in:
Slawek Kaplonski 2021-03-29 22:21:15 +02:00
parent 3e818718b6
commit ccea0021bc
3 changed files with 62 additions and 24 deletions

View File

@ -39,8 +39,11 @@ ICMPV6_ALLOWED_INGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY,
# List of ICMPv6 types that should be permitted (egress) by default. # List of ICMPv6 types that should be permitted (egress) by default.
ICMPV6_ALLOWED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY, ICMPV6_ALLOWED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY,
n_const.ICMPV6_TYPE_RS, n_const.ICMPV6_TYPE_RS,
n_const.ICMPV6_TYPE_NS, n_const.ICMPV6_TYPE_NS)
n_const.ICMPV6_TYPE_NA)
# List of ICMPv6 types that should be permitted depending on payload content
# to avoid spoofing (egress) by default.
ICMPV6_RESTRICTED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_NA, )
def port_sec_enabled(port): def port_sec_enabled(port):

View File

@ -892,8 +892,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
self._initialize_ingress(port) self._initialize_ingress(port)
def _initialize_egress_ipv6_icmp(self, port, allowed_pairs): def _initialize_egress_ipv6_icmp(self, port, allowed_pairs):
# NOTE(slaweq): should we include also fe80::/64 (link-local) subnet allowed_pairs = allowed_pairs.union({(port.mac, port.lla_address)})
# in the allowed pairs here?
for mac_addr, ip_addr in allowed_pairs: for mac_addr, ip_addr in allowed_pairs:
for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES: for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES:
self._add_flow( self._add_flow(
@ -909,6 +908,19 @@ class OVSFirewallDriver(firewall.FirewallDriver):
actions='resubmit(,%d)' % ( actions='resubmit(,%d)' % (
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE) ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE)
) )
for icmp_type in firewall.ICMPV6_RESTRICTED_EGRESS_TYPES:
self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=95,
in_port=port.ofport,
reg_port=port.ofport,
dl_type=constants.ETHERTYPE_IPV6,
nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
icmp_type=icmp_type,
nd_target=ip_addr,
actions='resubmit(,%d)' % (
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE)
)
def _initialize_egress_no_port_security(self, port_id, ovs_ports=None): def _initialize_egress_no_port_security(self, port_id, ovs_ports=None):
try: try:
@ -983,9 +995,9 @@ class OVSFirewallDriver(firewall.FirewallDriver):
"""Identify egress traffic and send it to egress base""" """Identify egress traffic and send it to egress base"""
# Apply mac/ip pairs for IPv4 # Apply mac/ip pairs for IPv4
allowed_pairs = port.allowed_pairs_v4.union( allowed_mac_ipv4_pairs = port.allowed_pairs_v4.union(
{(port.mac, ip_addr) for ip_addr in port.ipv4_addresses}) {(port.mac, ip_addr) for ip_addr in port.ipv4_addresses})
for mac_addr, ip_addr in allowed_pairs: for mac_addr, ip_addr in allowed_mac_ipv4_pairs:
self._add_flow( self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE, table=ovs_consts.BASE_EGRESS_TABLE,
priority=95, priority=95,
@ -1011,10 +1023,10 @@ class OVSFirewallDriver(firewall.FirewallDriver):
) )
# Apply mac/ip pairs for IPv6 # Apply mac/ip pairs for IPv6
allowed_pairs = port.allowed_pairs_v6.union( allowed_mac_ipv6_pairs = port.allowed_pairs_v6.union(
{(port.mac, ip_addr) for ip_addr in port.ipv6_addresses}) {(port.mac, ip_addr) for ip_addr in port.ipv6_addresses})
self._initialize_egress_ipv6_icmp(port, allowed_pairs) self._initialize_egress_ipv6_icmp(port, allowed_mac_ipv6_pairs)
for mac_addr, ip_addr in allowed_pairs: for mac_addr, ip_addr in allowed_mac_ipv6_pairs:
self._add_flow( self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE, table=ovs_consts.BASE_EGRESS_TABLE,
priority=65, priority=65,
@ -1029,9 +1041,18 @@ class OVSFirewallDriver(firewall.FirewallDriver):
) )
# DHCP discovery # DHCP discovery
for dl_type, src_port, dst_port in ( additional_ipv4_filters = [
(constants.ETHERTYPE_IP, 68, 67), {"dl_src": mac, "nw_src": ip}
(constants.ETHERTYPE_IPV6, 546, 547)): for mac, ip in allowed_mac_ipv4_pairs]
additional_ipv4_filters.append(
{"dl_src": port.mac, "nw_src": "0.0.0.0"})
additional_ipv6_filters = [
{"dl_src": mac, "ipv6_src": ip}
for mac, ip in allowed_mac_ipv6_pairs]
for dl_type, src_port, dst_port, filters_list in (
(constants.ETHERTYPE_IP, 68, 67, additional_ipv4_filters),
(constants.ETHERTYPE_IPV6, 546, 547, additional_ipv6_filters)):
for additional_filters in filters_list:
self._add_flow( self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE, table=ovs_consts.BASE_EGRESS_TABLE,
priority=80, priority=80,
@ -1042,7 +1063,8 @@ class OVSFirewallDriver(firewall.FirewallDriver):
tp_src=src_port, tp_src=src_port,
tp_dst=dst_port, tp_dst=dst_port,
actions='resubmit(,{:d})'.format( actions='resubmit(,{:d})'.format(
ovs_consts.ACCEPT_OR_INGRESS_TABLE) ovs_consts.ACCEPT_OR_INGRESS_TABLE),
**additional_filters
) )
# Ban dhcp service running on an instance # Ban dhcp service running on an instance
for dl_type, src_port, dst_port in ( for dl_type, src_port, dst_port in (

View File

@ -994,6 +994,19 @@ class TestOVSFirewallDriver(base.BaseTestCase):
ipv6_src='2003::1', ipv6_src='2003::1',
actions='resubmit(,%d)' % ( actions='resubmit(,%d)' % (
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE))) ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE)))
for icmp_type in firewall.ICMPV6_RESTRICTED_EGRESS_TYPES:
expected_calls.append(
mock.call(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=95,
in_port=TESTING_VLAN_TAG,
reg5=TESTING_VLAN_TAG,
dl_type='0x86dd',
nw_proto=constants.PROTO_NUM_IPV6_ICMP,
icmp_type=icmp_type,
nd_target='2003::1',
actions='resubmit(,%d)' % (
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE)))
self.mock_bridge.br.add_flow.assert_has_calls(expected_calls) self.mock_bridge.br.add_flow.assert_has_calls(expected_calls)
def test_process_trusted_ports_caches_port_id(self): def test_process_trusted_ports_caches_port_id(self):