c76164c058
The regular IPTablesFirewall needs zones to support safely clearly conntrack entries. In order to support the single bridge use case, the conntrack manager had to be refactored slightly to allow zones to be either unique to ports or unique to networks. Since all ports in a network share a bridge in the IPTablesDriver use case, a zone per port cannot be used since there is no way to distinguish which zone traffic should be checked against when traffic enters the bridge from outside the system. A zone per network is adequate for the single bridge per network solution since it implicitly does not suffer from the double-bridge cross in a single network that led to per port usage in OVS.[1] This had to adjust the functional firewall tests to use the correct bridge name now that it's relevant in the non hybrid IPTables case. 1. Ibe9e49653b2a280ea72cb95c2da64cd94c7739da Closes-Bug: #1668958 Closes-Bug: #1657260 Change-Id: Ie88237d3fe4807b712a7ec61eb932748c38952cc
549 lines
21 KiB
Python
549 lines
21 KiB
Python
# All Rights Reserved.
|
|
#
|
|
#
|
|
# 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.
|
|
import functools
|
|
|
|
import fixtures
|
|
import netaddr
|
|
from neutron_lib import constants
|
|
from oslo_utils import uuidutils
|
|
|
|
from neutron.agent import firewall
|
|
from neutron.common import constants as n_consts
|
|
from neutron.common import utils as common_utils
|
|
from neutron.tests.common import machine_fixtures
|
|
from neutron.tests.common import net_helpers
|
|
|
|
# NOTE: IPv6 uses NDP for obtaining destination endpoints link address that
|
|
# extends round-trip packet time in ICMP tests. The timeout value should be
|
|
# sufficient for correct scenarios but not too high because of negative
|
|
# tests.
|
|
ICMP_VERSION_TIMEOUTS = {
|
|
constants.IP_VERSION_4: 1,
|
|
constants.IP_VERSION_6: 2,
|
|
}
|
|
|
|
|
|
class ConnectionTesterException(Exception):
|
|
pass
|
|
|
|
|
|
def _validate_direction(f):
|
|
@functools.wraps(f)
|
|
def wrap(self, direction, *args, **kwargs):
|
|
if direction not in (firewall.INGRESS_DIRECTION,
|
|
firewall.EGRESS_DIRECTION):
|
|
raise ConnectionTesterException('Unknown direction %s' % direction)
|
|
return f(self, direction, *args, **kwargs)
|
|
return wrap
|
|
|
|
|
|
def _get_packets_sent_received(src_namespace, dst_ip, count):
|
|
pinger = net_helpers.Pinger(src_namespace, dst_ip, count=count)
|
|
pinger.start()
|
|
pinger.wait()
|
|
return pinger.sent, pinger.received
|
|
|
|
|
|
def all_replied(src_ns, dst_ip, count):
|
|
sent, received = _get_packets_sent_received(src_ns, dst_ip, count)
|
|
return sent == received
|
|
|
|
|
|
def all_lost(src_ns, dst_ip, count):
|
|
sent, received = _get_packets_sent_received(src_ns, dst_ip, count)
|
|
return received == 0
|
|
|
|
|
|
class ConnectionTester(fixtures.Fixture):
|
|
"""Base class for testers
|
|
|
|
This class implements API for various methods for testing connectivity. The
|
|
concrete implementation relies on how encapsulated resources are
|
|
configured. That means child classes should define resources by themselves
|
|
(e.g. endpoints connected through linux bridge or ovs bridge).
|
|
|
|
"""
|
|
|
|
UDP = net_helpers.NetcatTester.UDP
|
|
TCP = net_helpers.NetcatTester.TCP
|
|
ICMP = constants.PROTO_NAME_ICMP
|
|
ARP = n_consts.ETHERTYPE_NAME_ARP
|
|
INGRESS = firewall.INGRESS_DIRECTION
|
|
EGRESS = firewall.EGRESS_DIRECTION
|
|
|
|
def __init__(self, ip_cidr):
|
|
self.ip_cidr = ip_cidr
|
|
self.icmp_count = 3
|
|
self.connectivity_timeout = 12
|
|
|
|
def _setUp(self):
|
|
self._protocol_to_method = {
|
|
self.UDP: self._test_transport_connectivity,
|
|
self.TCP: self._test_transport_connectivity,
|
|
self.ICMP: self._test_icmp_connectivity,
|
|
self.ARP: self._test_arp_connectivity}
|
|
self._nc_testers = {}
|
|
self._pingers = {}
|
|
self.addCleanup(self.cleanup)
|
|
|
|
def cleanup(self):
|
|
for nc in self._nc_testers.values():
|
|
nc.stop_processes()
|
|
for pinger in self._pingers.values():
|
|
pinger.stop()
|
|
|
|
@property
|
|
def vm_namespace(self):
|
|
return self._vm.namespace
|
|
|
|
@property
|
|
def vm_ip_address(self):
|
|
return self._vm.ip
|
|
|
|
@property
|
|
def vm_ip_cidr(self):
|
|
return self._vm.ip_cidr
|
|
|
|
@vm_ip_cidr.setter
|
|
def vm_ip_cidr(self, ip_cidr):
|
|
self._vm.ip_cidr = ip_cidr
|
|
|
|
@property
|
|
def vm_mac_address(self):
|
|
return self._vm.port.link.address
|
|
|
|
@vm_mac_address.setter
|
|
def vm_mac_address(self, mac_address):
|
|
self._vm.mac_address = mac_address
|
|
|
|
@property
|
|
def peer_mac_address(self):
|
|
return self._peer.port.link.address
|
|
|
|
@peer_mac_address.setter
|
|
def peer_mac_address(self, mac_address):
|
|
self._peer.mac_address = mac_address
|
|
|
|
@property
|
|
def peer_namespace(self):
|
|
return self._peer.namespace
|
|
|
|
@property
|
|
def peer_ip_address(self):
|
|
return self._peer.ip
|
|
|
|
def set_vm_default_gateway(self, default_gw):
|
|
self._vm.set_default_gateway(default_gw)
|
|
|
|
def flush_arp_tables(self):
|
|
"""Flush arptables in all used namespaces"""
|
|
for machine in (self._peer, self._vm):
|
|
machine.port.neigh.flush(4, 'all')
|
|
|
|
def _test_transport_connectivity(self, direction, protocol, src_port,
|
|
dst_port):
|
|
nc_tester = self._create_nc_tester(direction, protocol, src_port,
|
|
dst_port)
|
|
try:
|
|
nc_tester.test_connectivity()
|
|
except RuntimeError as exc:
|
|
nc_tester.stop_processes()
|
|
raise ConnectionTesterException(
|
|
"%s connection over %s protocol with %s source port and "
|
|
"%s destination port can't be established: %s" % (
|
|
direction, protocol, src_port, dst_port, exc))
|
|
|
|
@_validate_direction
|
|
def _get_namespace_and_address(self, direction):
|
|
if direction == self.INGRESS:
|
|
return self.peer_namespace, self.vm_ip_address
|
|
return self.vm_namespace, self.peer_ip_address
|
|
|
|
def _test_icmp_connectivity(self, direction, protocol, src_port, dst_port):
|
|
src_namespace, ip_address = self._get_namespace_and_address(direction)
|
|
ip_version = common_utils.get_ip_version(ip_address)
|
|
icmp_timeout = ICMP_VERSION_TIMEOUTS[ip_version]
|
|
try:
|
|
net_helpers.assert_ping(src_namespace, ip_address,
|
|
timeout=icmp_timeout)
|
|
except RuntimeError:
|
|
raise ConnectionTesterException(
|
|
"ICMP packets can't get from %s namespace to %s address" % (
|
|
src_namespace, ip_address))
|
|
|
|
def _test_arp_connectivity(self, direction, protocol, src_port, dst_port):
|
|
src_namespace, ip_address = self._get_namespace_and_address(direction)
|
|
try:
|
|
net_helpers.assert_arping(src_namespace, ip_address)
|
|
except RuntimeError:
|
|
raise ConnectionTesterException(
|
|
"ARP queries to %s address have no response from %s namespace"
|
|
% (ip_address, src_namespace))
|
|
|
|
@_validate_direction
|
|
def assert_connection(self, direction, protocol, src_port=None,
|
|
dst_port=None):
|
|
testing_method = self._protocol_to_method[protocol]
|
|
testing_method(direction, protocol, src_port, dst_port)
|
|
|
|
def assert_no_connection(self, direction, protocol, src_port=None,
|
|
dst_port=None):
|
|
try:
|
|
self.assert_connection(direction, protocol, src_port, dst_port)
|
|
except ConnectionTesterException:
|
|
pass
|
|
else:
|
|
dst_port_info = str()
|
|
src_port_info = str()
|
|
if dst_port is not None:
|
|
dst_port_info = " and destination port %d" % dst_port
|
|
if src_port is not None:
|
|
src_port_info = " and source port %d" % src_port
|
|
raise ConnectionTesterException("%s connection with protocol %s, "
|
|
"source port %s, destination "
|
|
"port %s was established but it "
|
|
"shouldn't be possible" % (
|
|
direction, protocol,
|
|
src_port_info, dst_port_info))
|
|
|
|
@_validate_direction
|
|
def assert_established_connection(self, direction, protocol, src_port=None,
|
|
dst_port=None):
|
|
nc_params = (direction, protocol, src_port, dst_port)
|
|
nc_tester = self._nc_testers.get(nc_params)
|
|
if nc_tester:
|
|
if nc_tester.is_established:
|
|
try:
|
|
nc_tester.test_connectivity()
|
|
except RuntimeError:
|
|
raise ConnectionTesterException(
|
|
"Established %s connection with protocol %s, source "
|
|
"port %s and destination port %s can no longer "
|
|
"communicate")
|
|
else:
|
|
nc_tester.stop_processes()
|
|
raise ConnectionTesterException(
|
|
'%s connection with protocol %s, source port %s and '
|
|
'destination port %s is not established' % nc_params)
|
|
else:
|
|
raise ConnectionTesterException(
|
|
"Attempting to test established %s connection with protocol %s"
|
|
", source port %s and destination port %s that hasn't been "
|
|
"established yet by calling establish_connection()"
|
|
% nc_params)
|
|
|
|
def assert_no_established_connection(self, direction, protocol,
|
|
src_port=None, dst_port=None):
|
|
try:
|
|
self.assert_established_connection(direction, protocol, src_port,
|
|
dst_port)
|
|
except ConnectionTesterException:
|
|
pass
|
|
else:
|
|
raise ConnectionTesterException(
|
|
'Established %s connection with protocol %s, source port %s, '
|
|
'destination port %s can still send packets through' % (
|
|
direction, protocol, src_port, dst_port))
|
|
|
|
@_validate_direction
|
|
def establish_connection(self, direction, protocol, src_port=None,
|
|
dst_port=None):
|
|
nc_tester = self._create_nc_tester(direction, protocol, src_port,
|
|
dst_port)
|
|
nc_tester.establish_connection()
|
|
self.addCleanup(nc_tester.stop_processes)
|
|
|
|
def _create_nc_tester(self, direction, protocol, src_port, dst_port):
|
|
"""Create netcat tester
|
|
|
|
If there already exists a netcat tester that has established
|
|
connection, exception is raised.
|
|
"""
|
|
nc_key = (direction, protocol, src_port, dst_port)
|
|
nc_tester = self._nc_testers.get(nc_key)
|
|
if nc_tester and nc_tester.is_established:
|
|
raise ConnectionTesterException(
|
|
'%s connection using %s protocol, source port %s and '
|
|
'destination port %s is already established' % (
|
|
direction, protocol, src_port, dst_port))
|
|
|
|
if direction == self.INGRESS:
|
|
client_ns = self.peer_namespace
|
|
server_ns = self.vm_namespace
|
|
server_addr = self.vm_ip_address
|
|
else:
|
|
client_ns = self.vm_namespace
|
|
server_ns = self.peer_namespace
|
|
server_addr = self.peer_ip_address
|
|
|
|
server_port = dst_port or net_helpers.get_free_namespace_port(
|
|
protocol, server_ns)
|
|
nc_tester = net_helpers.NetcatTester(client_namespace=client_ns,
|
|
server_namespace=server_ns,
|
|
address=server_addr,
|
|
protocol=protocol,
|
|
src_port=src_port,
|
|
dst_port=server_port)
|
|
self._nc_testers[nc_key] = nc_tester
|
|
return nc_tester
|
|
|
|
def _get_pinger(self, direction):
|
|
try:
|
|
pinger = self._pingers[direction]
|
|
except KeyError:
|
|
src_namespace, dst_address = self._get_namespace_and_address(
|
|
direction)
|
|
pinger = net_helpers.Pinger(src_namespace, dst_address)
|
|
self._pingers[direction] = pinger
|
|
return pinger
|
|
|
|
def start_sending_icmp(self, direction):
|
|
pinger = self._get_pinger(direction)
|
|
pinger.start()
|
|
|
|
def stop_sending_icmp(self, direction):
|
|
pinger = self._get_pinger(direction)
|
|
pinger.stop()
|
|
|
|
def get_sent_icmp_packets(self, direction):
|
|
pinger = self._get_pinger(direction)
|
|
return pinger.sent
|
|
|
|
def get_received_icmp_packets(self, direction):
|
|
pinger = self._get_pinger(direction)
|
|
return pinger.received
|
|
|
|
def assert_net_unreachable(self, direction, destination):
|
|
src_namespace, dst_address = self._get_namespace_and_address(
|
|
direction)
|
|
pinger = net_helpers.Pinger(src_namespace, destination, count=5)
|
|
pinger.start()
|
|
pinger.wait()
|
|
if not pinger.destination_unreachable:
|
|
raise ConnectionTesterException(
|
|
'No Host Destination Unreachable packets were received when '
|
|
'sending icmp packets to %s' % destination)
|
|
|
|
def wait_for_connection(self, direction):
|
|
src_ns, dst_ip = self._get_namespace_and_address(
|
|
direction)
|
|
all_replied_predicate = functools.partial(
|
|
all_replied, src_ns, dst_ip, count=self.icmp_count)
|
|
common_utils.wait_until_true(
|
|
all_replied_predicate, timeout=self.connectivity_timeout,
|
|
exception=ConnectionTesterException(
|
|
"Not all ICMP packets replied from %s namespace to %s "
|
|
"address." % self._get_namespace_and_address(direction)))
|
|
|
|
def wait_for_no_connection(self, direction):
|
|
src_ns, dst_ip = self._get_namespace_and_address(
|
|
direction)
|
|
all_lost_predicate = functools.partial(
|
|
all_lost, src_ns, dst_ip, count=self.icmp_count)
|
|
common_utils.wait_until_true(
|
|
all_lost_predicate, timeout=self.connectivity_timeout,
|
|
exception=ConnectionTesterException(
|
|
"At least one packet got reply from %s namespace to %s "
|
|
"address." % self._get_namespace_and_address(direction)))
|
|
|
|
|
|
class OVSBaseConnectionTester(ConnectionTester):
|
|
|
|
@property
|
|
def peer_port_id(self):
|
|
return self._peer.port.id
|
|
|
|
@property
|
|
def vm_port_id(self):
|
|
return self._vm.port.id
|
|
|
|
@staticmethod
|
|
def set_tag(port_name, bridge, tag):
|
|
ovsdb = bridge.ovsdb
|
|
with ovsdb.transaction() as txn:
|
|
txn.add(ovsdb.db_set('Port', port_name, ('tag', tag)))
|
|
txn.add(
|
|
ovsdb.db_add(
|
|
'Port', port_name, 'other_config', {'tag': str(tag)}))
|
|
|
|
|
|
class OVSConnectionTester(OVSBaseConnectionTester):
|
|
|
|
"""Tester with OVS bridge in the middle
|
|
|
|
The endpoints are created as OVS ports attached to the OVS bridge.
|
|
|
|
NOTE: The OVS ports are connected from the namespace. This connection is
|
|
currently not supported in OVS and may lead to unpredicted behavior:
|
|
https://bugzilla.redhat.com/show_bug.cgi?id=1160340
|
|
|
|
"""
|
|
|
|
def _setUp(self):
|
|
super(OVSConnectionTester, self)._setUp()
|
|
self.bridge = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
|
|
machines = self.useFixture(
|
|
machine_fixtures.PeerMachines(
|
|
self.bridge, self.ip_cidr)).machines
|
|
self._peer = machines[0]
|
|
self._vm = machines[1]
|
|
self._set_port_attrs(self._peer.port)
|
|
self._set_port_attrs(self._vm.port)
|
|
|
|
def _set_port_attrs(self, port):
|
|
port.id = uuidutils.generate_uuid()
|
|
attrs = [('type', 'internal'),
|
|
('external_ids', {
|
|
'iface-id': port.id,
|
|
'iface-status': 'active',
|
|
'attached-mac': port.link.address})]
|
|
for column, value in attrs:
|
|
self.bridge.set_db_attribute('Interface', port.name, column, value)
|
|
|
|
def set_vm_tag(self, tag):
|
|
self.set_tag(self._vm.port.name, self.bridge, tag)
|
|
|
|
def set_peer_tag(self, tag):
|
|
self.set_tag(self._peer.port.name, self.bridge, tag)
|
|
|
|
|
|
class OVSTrunkConnectionTester(OVSBaseConnectionTester):
|
|
"""Tester with OVS bridge and a trunk bridge
|
|
|
|
Two endpoints: one is a VM that is connected to a port associated with a
|
|
trunk (the port is created on the trunk bridge), the other is a VM on the
|
|
same network (the port is on the integration bridge).
|
|
|
|
NOTE: The OVS ports are connected from the namespace. This connection is
|
|
currently not supported in OVS and may lead to unpredicted behavior:
|
|
https://bugzilla.redhat.com/show_bug.cgi?id=1160340
|
|
|
|
"""
|
|
|
|
def __init__(self, ip_cidr, br_trunk_name):
|
|
super(OVSTrunkConnectionTester, self).__init__(ip_cidr)
|
|
self._br_trunk_name = br_trunk_name
|
|
|
|
def _setUp(self):
|
|
super(OVSTrunkConnectionTester, self)._setUp()
|
|
self.bridge = self.useFixture(
|
|
net_helpers.OVSBridgeFixture()).bridge
|
|
self.br_trunk = self.useFixture(
|
|
net_helpers.OVSTrunkBridgeFixture(self._br_trunk_name)).bridge
|
|
self._peer = self.useFixture(machine_fixtures.FakeMachine(
|
|
self.bridge, self.ip_cidr))
|
|
ip_cidr = net_helpers.increment_ip_cidr(self.ip_cidr, 1)
|
|
|
|
self._vm = self.useFixture(machine_fixtures.FakeMachine(
|
|
self.br_trunk, ip_cidr))
|
|
|
|
def add_vlan_interface_and_peer(self, vlan, ip_cidr):
|
|
"""Create a sub_port and a peer
|
|
|
|
We create a sub_port that uses vlan as segmentation ID. In the vm
|
|
namespace we create a vlan subinterface on the same vlan.
|
|
A peer on the same network is created. When pinging from the peer
|
|
to the sub_port packets will be tagged using the internal vlan ID
|
|
of the network. The sub_port will remove that vlan tag and push the
|
|
vlan specified in the segmentation ID. The packets will finally reach
|
|
the vlan subinterface in the vm namespace.
|
|
|
|
"""
|
|
|
|
network = netaddr.IPNetwork(ip_cidr)
|
|
net_helpers.create_vlan_interface(
|
|
self._vm.namespace, self._vm.port.name,
|
|
self.vm_mac_address, network, vlan)
|
|
self._ip_vlan = str(network.ip)
|
|
ip_cidr = net_helpers.increment_ip_cidr(ip_cidr, 1)
|
|
self._peer2 = self.useFixture(machine_fixtures.FakeMachine(
|
|
self.bridge, ip_cidr))
|
|
|
|
def set_vm_tag(self, tag):
|
|
self.set_tag(self._vm.port.name, self.br_trunk, tag)
|
|
|
|
def set_peer_tag(self, tag):
|
|
self.set_tag(self._peer.port.name, self.bridge, tag)
|
|
|
|
def _get_subport_namespace_and_address(self, direction):
|
|
if direction == self.INGRESS:
|
|
return self._peer2.namespace, self._ip_vlan
|
|
return self._vm.namespace, self._peer2.ip
|
|
|
|
def wait_for_sub_port_connectivity(self, direction):
|
|
src_ns, dst_ip = self._get_subport_namespace_and_address(
|
|
direction)
|
|
all_replied_predicate = functools.partial(
|
|
all_replied, src_ns, dst_ip, count=self.icmp_count)
|
|
common_utils.wait_until_true(
|
|
all_replied_predicate, timeout=self.connectivity_timeout,
|
|
exception=ConnectionTesterException(
|
|
"ICMP traffic from %s namespace to subport with address %s "
|
|
"can't get through." % (src_ns, dst_ip)))
|
|
|
|
def wait_for_sub_port_no_connectivity(self, direction):
|
|
src_ns, dst_ip = self._get_subport_namespace_and_address(
|
|
direction)
|
|
all_lost_predicate = functools.partial(
|
|
all_lost, src_ns, dst_ip, count=self.icmp_count)
|
|
common_utils.wait_until_true(
|
|
all_lost_predicate, timeout=self.connectivity_timeout,
|
|
exception=ConnectionTesterException(
|
|
"ICMP traffic from %s namespace to subport with address %s "
|
|
"can still get through." % (src_ns, dst_ip)))
|
|
|
|
|
|
class LinuxBridgeConnectionTester(ConnectionTester):
|
|
"""Tester with linux bridge in the middle
|
|
|
|
Both endpoints are placed in their separated namespace connected to
|
|
bridge's namespace via veth pair.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.bridge_name = kwargs.pop('bridge_name', None)
|
|
super(LinuxBridgeConnectionTester, self).__init__(*args, **kwargs)
|
|
|
|
def _setUp(self):
|
|
super(LinuxBridgeConnectionTester, self)._setUp()
|
|
bridge_args = {}
|
|
if self.bridge_name:
|
|
bridge_args = {'prefix': self.bridge_name,
|
|
'prefix_is_full_name': True}
|
|
self.bridge = self.useFixture(
|
|
net_helpers.LinuxBridgeFixture(**bridge_args)).bridge
|
|
machines = self.useFixture(
|
|
machine_fixtures.PeerMachines(
|
|
self.bridge, self.ip_cidr)).machines
|
|
self._peer = machines[0]
|
|
self._vm = machines[1]
|
|
|
|
@property
|
|
def bridge_namespace(self):
|
|
return self.bridge.namespace
|
|
|
|
@property
|
|
def vm_port_id(self):
|
|
return net_helpers.VethFixture.get_peer_name(self._vm.port.name)
|
|
|
|
@property
|
|
def peer_port_id(self):
|
|
return net_helpers.VethFixture.get_peer_name(self._peer.port.name)
|
|
|
|
def flush_arp_tables(self):
|
|
self.bridge.neigh.flush(4, 'all')
|
|
super(LinuxBridgeConnectionTester, self).flush_arp_tables()
|