[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

Change-Id: Iffb6643359562487414460f5a7e19a7fae9f935c
This commit is contained in:
Slawek Kaplonski 2021-03-29 22:21:15 +02:00
parent 377d8d5c83
commit ca7822e210
3 changed files with 61 additions and 24 deletions

View File

@ -34,8 +34,11 @@ ICMPV6_ALLOWED_INGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY,
# List of ICMPv6 types that should be permitted (egress) by default.
ICMPV6_ALLOWED_EGRESS_TYPES = (n_const.ICMPV6_TYPE_MLD_QUERY,
n_const.ICMPV6_TYPE_RS,
n_const.ICMPV6_TYPE_NS,
n_const.ICMPV6_TYPE_NA)
n_const.ICMPV6_TYPE_NS)
# 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):

View File

@ -934,8 +934,7 @@ class OVSFirewallDriver(firewall.FirewallDriver):
self._initialize_ingress(port)
def _initialize_egress_ipv6_icmp(self, port, allowed_pairs):
# NOTE(slaweq): should we include also fe80::/64 (link-local) subnet
# in the allowed pairs here?
allowed_pairs = allowed_pairs.union({(port.mac, port.lla_address)})
for mac_addr, ip_addr in allowed_pairs:
for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES:
self._add_flow(
@ -951,6 +950,19 @@ class OVSFirewallDriver(firewall.FirewallDriver):
actions='resubmit(,%d)' % (
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=lib_const.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):
try:
@ -1025,9 +1037,9 @@ class OVSFirewallDriver(firewall.FirewallDriver):
"""Identify egress traffic and send it to egress base"""
# 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})
for mac_addr, ip_addr in allowed_pairs:
for mac_addr, ip_addr in allowed_mac_ipv4_pairs:
self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=95,
@ -1053,10 +1065,10 @@ class OVSFirewallDriver(firewall.FirewallDriver):
)
# 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})
self._initialize_egress_ipv6_icmp(port, allowed_pairs)
for mac_addr, ip_addr in allowed_pairs:
self._initialize_egress_ipv6_icmp(port, allowed_mac_ipv6_pairs)
for mac_addr, ip_addr in allowed_mac_ipv6_pairs:
self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=65,
@ -1071,21 +1083,30 @@ class OVSFirewallDriver(firewall.FirewallDriver):
)
# DHCP discovery
for dl_type, src_port, dst_port in (
(lib_const.ETHERTYPE_IP, 68, 67),
(lib_const.ETHERTYPE_IPV6, 546, 547)):
self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=80,
reg_port=port.ofport,
in_port=port.ofport,
dl_type=dl_type,
nw_proto=lib_const.PROTO_NUM_UDP,
tp_src=src_port,
tp_dst=dst_port,
actions='resubmit(,{:d})'.format(
ovs_consts.ACCEPT_OR_INGRESS_TABLE)
)
additional_ipv4_filters = [
{"dl_src": mac, "nw_src": ip}
for mac, ip in (*allowed_mac_ipv4_pairs,
(port.mac, '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 (
(lib_const.ETHERTYPE_IP, 68, 67, additional_ipv4_filters),
(lib_const.ETHERTYPE_IPV6, 546, 547, additional_ipv6_filters)):
for additional_filters in filters_list:
self._add_flow(
table=ovs_consts.BASE_EGRESS_TABLE,
priority=80,
reg_port=port.ofport,
in_port=port.ofport,
dl_type=dl_type,
**additional_filters,
nw_proto=lib_const.PROTO_NUM_UDP,
tp_src=src_port,
tp_dst=dst_port,
actions='resubmit(,{:d})'.format(
ovs_consts.ACCEPT_OR_INGRESS_TABLE)
)
# Ban dhcp service running on an instance
for dl_type, src_port, dst_port in (
(lib_const.ETHERTYPE_IP, 67, 68),

View File

@ -1057,6 +1057,19 @@ class TestOVSFirewallDriver(base.BaseTestCase):
ipv6_src='2003::1',
actions='resubmit(,%d)' % (
ovs_consts.ACCEPTED_EGRESS_TRAFFIC_NORMAL_TABLE)))
for icmp_type in agent_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)
def test_process_trusted_ports_caches_port_id(self):