diff options
author | Sachin Saxena <sachin.saxena@freescale.com> | 2018-02-28 20:28:52 +0530 |
---|---|---|
committer | Sachin Saxena <sachin.saxena@nxp.com> | 2018-02-28 20:34:56 +0530 |
commit | 0689fce93ba269c48f83a2f70f971b3976d04c90 (patch) | |
tree | 4cc2908df3598507cc1828ac19d8c43b22450ffa /test | |
parent | 746b57564deede624261ab8a96c94f562f24d22c (diff) | |
parent | d594711a5d79859a7d0bde83a516f7ab52051d9b (diff) |
Merge branch 'stable/1710' of https://gerrit.fd.io/r/vpp into 17101710
Diffstat (limited to 'test')
87 files changed, 41167 insertions, 0 deletions
diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 00000000..da77accc --- /dev/null +++ b/test/Makefile @@ -0,0 +1,241 @@ +.PHONY: verify-python-path + +VPP_TEST_FAILED_DIR=/tmp/vpp-failed-unittests/ + +verify-python-path: +ifndef VPP_PYTHON_PREFIX + $(error VPP_PYTHON_PREFIX is not set) +endif + +.PHONY: verify-no-running-vpp + +ifdef VPP_ZOMBIE_NOCHECK +VPP_PIDS= +else +VPP_PIDS=$(shell pgrep -d, -x vpp_main) +endif + +ifeq ($(DEBUG),gdb) +FORCE_FOREGROUND=1 +else ifeq ($(DEBUG),gdbserver) +FORCE_FOREGROUND=1 +else +FORCE_FOREGROUND=0 +endif + +verify-no-running-vpp: + @if [ "$(VPP_PIDS)" != "" ]; then \ + echo; \ + echo "*** Existing vpp processes detected (PID(s): $(VPP_PIDS)). Running tests under these conditions is not supported. ***"; \ + echo; \ + ps -fp $(VPP_PIDS);\ + echo; \ + false; \ + fi + +UNITTEST_EXTRA_OPTS= +UNITTEST_FAILFAST_OPTS= + +ifeq ($(FAILFAST),1) +UNITTEST_EXTRA_OPTS=-f +endif + +ifneq ($(EXTERN_TESTS),) +UNITTEST_EXTRA_OPTS=$(UNITTEST_FAILFAST_OPTS) -d $(EXTERN_TESTS) +endif + +PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv +PYTHON_DEPENDS=faulthandler six scapy==2.3.3 pexpect subprocess32 cffi git+https://github.com/klement/py-lispnetworking@setup +SCAPY_SOURCE=$(shell find $(PYTHON_VENV_PATH) -name site-packages) +BUILD_COV_DIR=$(BR)/test-cov + +GET_PIP_SCRIPT=$(VPP_PYTHON_PREFIX)/get-pip.py +PIP_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/pip-install.done +PIP_PATCH_DONE=$(VPP_PYTHON_PREFIX)/pip-patch.done +PAPI_INSTALL_DONE=$(VPP_PYTHON_PREFIX)/papi-install.done + +PAPI_INSTALL_FLAGS=$(PIP_INSTALL_DONE) $(PIP_PATCH_DONE) $(PAPI_INSTALL_DONE) + +ifeq ($(PYTHON),) +PYTHON_INTERP=python2.7 +else +PYTHON_INTERP=$(PYTHON) +endif + +$(GET_PIP_SCRIPT): + @mkdir -p $(VPP_PYTHON_PREFIX) + @bash -c "cd $(VPP_PYTHON_PREFIX) && curl -O https://bootstrap.pypa.io/get-pip.py" + +$(PIP_INSTALL_DONE): $(GET_PIP_SCRIPT) + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python $(GET_PIP_SCRIPT)" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install $(PYTHON_DEPENDS)" + @touch $@ + +$(PIP_PATCH_DONE): $(PIP_INSTALL_DONE) + @echo --- patching --- + @sleep 1 # Ensure python recompiles patched *.py files -> *.pyc + for f in $(CURDIR)/patches/scapy-2.3.3/*.patch ; do \ + echo Applying patch: $$(basename $$f) ; \ + patch -p1 -d $(SCAPY_SOURCE) < $$f ; \ + done + @touch $@ + +$(PAPI_INSTALL_DONE): $(PIP_PATCH_DONE) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && cd $(WS_ROOT)/src/vpp-api/python && python setup.py install" + @touch $@ + +define retest-func + @env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) scripts/setsid_wrapper.sh $(FORCE_FOREGROUND) $(PYTHON_VENV_PATH)/bin/activate python run_tests.py -d $(TEST_DIR) $(UNITTEST_EXTRA_OPTS) || env VPP_TEST_FAILED_DIR=$(VPP_TEST_FAILED_DIR) COMPRESS_FAILED_TEST_LOGS=$(COMPRESS_FAILED_TEST_LOGS) scripts/compress_failed.sh +endef + +.PHONY: sanity + +sanity: verify-no-running-vpp + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python sanity_import_vpp_papi.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Sanity check failed, cannot import vpp_papi\" &&\ + echo \"* to debug: \" &&\ + echo \"* 1. enter test shell: make test-shell\" &&\ + echo \"* 2. execute debugger: gdb python -ex 'run sanity_import_vpp_papi.py'\" &&\ + echo \"*******************************************************************\" &&\ + false)" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python sanity_run_vpp.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Sanity check failed, cannot run vpp\" &&\ + echo \"*******************************************************************\" &&\ + false)" + +.PHONY: ext +ext: + make -C ext + +test: verify-python-path $(PAPI_INSTALL_DONE) ext sanity reset + $(call retest-func) + +retest: verify-python-path sanity reset + $(call retest-func) + +shell: verify-python-path $(PAPI_INSTALL_DONE) + @echo "source $(PYTHON_VENV_PATH)/bin/activate;\ + echo '***';\ + echo VPP_TEST_BUILD_DIR=$(VPP_TEST_BUILD_DIR);\ + echo VPP_TEST_BIN=$(VPP_TEST_BIN);\ + echo VPP_TEST_PLUGIN_PATH=$(VPP_TEST_PLUGIN_PATH);\ + echo VPP_TEST_INSTALL_PATH=$(VPP_TEST_INSTALL_PATH);\ + echo EXTERN_TESTS=$(EXTERN_TESTS);\ + echo EXTERN_PLUGINS=$(EXTERN_PLUGINS);\ + echo EXTERN_COV_DIR=$(EXTERN_COV_DIR);\ + echo LD_LIBRARY_PATH=$(LD_LIBRARY_PATH);\ + echo '***';\ + exec </dev/tty" | bash -i + +.PHONY: wipe doc + +reset: + @rm -f /dev/shm/vpp-unittest-* + @rm -rf /tmp/vpp-unittest-* + @rm -rf $(VPP_TEST_FAILED_DIR) + @mkdir $(VPP_TEST_FAILED_DIR) + +wipe: reset + @make -C ext clean + @rm -rf $(PYTHON_VENV_PATH) + @rm -f $(PAPI_INSTALL_FLAGS) + +doc: verify-python-path $(PIP_PATCH_DONE) + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx sphinx-rtd-theme" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" + +.PHONY: wipe-doc + +wipe-doc: + @make -C doc wipe BR=$(BR) + +cov: wipe-cov reset verify-python-path $(PAPI_INSTALL_DONE) + @lcov --zerocounters --directory $(VPP_TEST_BUILD_DIR) + @test -z "$(EXTERN_COV_DIR)" || lcov --zerocounters --directory $(EXTERN_COV_DIR) + $(call retest-func) + @mkdir $(BUILD_COV_DIR) + @lcov --capture --directory $(VPP_TEST_BUILD_DIR) --output-file $(BUILD_COV_DIR)/coverage.info + @test -z "$(EXTERN_COV_DIR)" || lcov --capture --directory $(EXTERN_COV_DIR) --output-file $(BUILD_COV_DIR)/extern-coverage.info + @genhtml $(BUILD_COV_DIR)/coverage.info --output-directory $(BUILD_COV_DIR)/html + @test -z "$(EXTERN_COV_DIR)" || genhtml $(BUILD_COV_DIR)/extern-coverage.info --output-directory $(BUILD_COV_DIR)/extern-html + @echo + @echo "Build finished. Code coverage report is in $(BUILD_COV_DIR)/html/index.html" + @test -z "$(EXTERN_COV_DIR)" || echo "Code coverage report for out-of-tree objects is in $(BUILD_COV_DIR)/extern-html/index.html" + +.PHONY: wipe-cov + +wipe-cov: wipe + @rm -rf $(BUILD_COV_DIR) + +.PHONY: checkstyle +checkstyle: verify-python-path + @virtualenv $(PYTHON_VENV_PATH) -p $(PYTHON_INTERP) + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install pep8" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate &&\ + pep8 --show-source -v $(WS_ROOT)/test/*.py ||\ + (echo \"*******************************************************************\" &&\ + echo \"* Test framework PEP8 compliance check FAILED \" &&\ + echo \"*******************************************************************\" &&\ + false)" + @echo "*******************************************************************" + @echo "* Test framework PEP8 compliance check passed" + @echo "*******************************************************************" + +help: + @echo "Running tests:" + @echo "" + @echo " test - build and run (basic) functional tests" + @echo " test-debug - build and run (basic) functional tests (debug build)" + @echo " test-all - build and run (all) functional tests" + @echo " test-all-debug - build and run (all) functional tests (debug build)" + @echo " retest - run functional tests" + @echo " retest-debug - run functional tests (debug build)" + @echo " test-wipe - wipe (temporary) files generated by unit tests" + @echo " test-shell - enter shell with test environment" + @echo " test-shell-debug - enter shell with test environment (debug build)" + @echo "" + @echo "Arguments controlling test runs:" + @echo " V=[0|1|2] - set test verbosity level" + @echo " FAILFAST=[0|1] - fail fast if 1, complete all tests if 0" + @echo " TIMEOUT=<timeout> - fail test suite if any single test takes longer than <timeout> to finish" + @echo " DEBUG=<type> - set VPP debugging kind" + @echo " DEBUG=core - detect coredump and load it in gdb on crash" + @echo " DEBUG=gdb - allow easy debugging by printing VPP PID " + @echo " and waiting for user input before running " + @echo " and tearing down a testcase" + @echo " DEBUG=gdbserver - run gdb inside a gdb server, otherwise " + @echo " same as above" + @echo " STEP=[yes|no] - ease debugging by stepping through a testcase " + @echo " TEST=<filter> - filter the set of tests:" + @echo " by file-name - only run tests from specified file, e.g. TEST=test_bfd selects all tests from test_bfd.py" + @echo " by file-suffix - same as file-name, but 'test_' is omitted e.g. TEST=bfd selects all tests from test_bfd.py" + @echo " by wildcard - wildcard filter is <file>.<class>.<test function>, each can be replaced by '*'" + @echo " e.g. TEST='test_bfd.*.*' is equivalent to above example of filter by file-name" + @echo " TEST='bfd.*.*' is equivalent to above example of filter by file-suffix" + @echo " TEST='bfd.BFDAPITestCase.*' selects all tests from test_bfd.py which are part of BFDAPITestCase class" + @echo " TEST='bfd.BFDAPITestCase.test_add_bfd' selects a single test named test_add_bfd from test_bfd.py/BFDAPITestCase" + @echo " TEST='*.*.test_add_bfd' selects all test functions named test_add_bfd from all files/classes" + @echo "" + @echo " VPP_ZOMBIE_NOCHECK=1 - skip checking for vpp (zombie) processes (CAUTION)" + @echo " COREDUMP_SIZE=<size> - pass <size> as unix { coredump-size <size> } argument to vpp" + @echo " e.g. COREDUMP_SIZE=4g" + @echo " COREDUMP_SIZE=unlimited" + @echo " EXTERN_TESTS=<path> - path to out-of-tree test_<name>.py files containing test cases" + @echo " EXTERN_PLUGINS=<path>- path to out-of-tree plugins to be loaded by vpp under test" + @echo " EXTERN_COV_DIR=<path>- path to out-of-tree prefix, where source, object and .gcda files can be found for coverage report" + @echo "" + @echo "Creating test documentation" + @echo " test-doc - generate documentation for test framework" + @echo " test-wipe-doc - wipe documentation for test framework" + @echo "" + @echo "Creating test code coverage report" + @echo " test-cov - generate code coverage report for test framework" + @echo " test-wipe-cov - wipe code coverage report for test framework" + @echo "" + @echo "Verifying code-style" + @echo " test-checkstyle - check PEP8 compliance" + @echo "" diff --git a/test/bfd.py b/test/bfd.py new file mode 100644 index 00000000..452a1804 --- /dev/null +++ b/test/bfd.py @@ -0,0 +1,459 @@ +""" BFD protocol implementation """ + +from random import randint +from socket import AF_INET, AF_INET6, inet_pton +from scapy.all import bind_layers +from scapy.layers.inet import UDP +from scapy.packet import Packet +from scapy.fields import BitField, BitEnumField, XByteField, FlagsField,\ + ConditionalField, StrField +from vpp_object import VppObject +from util import NumericConstant + + +class BFDDiagCode(NumericConstant): + """ BFD Diagnostic Code """ + no_diagnostic = 0 + control_detection_time_expired = 1 + echo_function_failed = 2 + neighbor_signaled_session_down = 3 + forwarding_plane_reset = 4 + path_down = 5 + concatenated_path_down = 6 + administratively_down = 7 + reverse_concatenated_path_down = 8 + + desc_dict = { + no_diagnostic: "No diagnostic", + control_detection_time_expired: "Control Detection Time Expired", + echo_function_failed: "Echo Function Failed", + neighbor_signaled_session_down: "Neighbor Signaled Session Down", + forwarding_plane_reset: "Forwarding Plane Reset", + path_down: "Path Down", + concatenated_path_down: "Concatenated Path Down", + administratively_down: "Administratively Down", + reverse_concatenated_path_down: "Reverse Concatenated Path Down", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFDState(NumericConstant): + """ BFD State """ + admin_down = 0 + down = 1 + init = 2 + up = 3 + + desc_dict = { + admin_down: "AdminDown", + down: "Down", + init: "Init", + up: "Up", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +class BFDAuthType(NumericConstant): + """ BFD Authentication Type """ + no_auth = 0 + simple_pwd = 1 + keyed_md5 = 2 + meticulous_keyed_md5 = 3 + keyed_sha1 = 4 + meticulous_keyed_sha1 = 5 + + desc_dict = { + no_auth: "No authentication", + simple_pwd: "Simple Password", + keyed_md5: "Keyed MD5", + meticulous_keyed_md5: "Meticulous Keyed MD5", + keyed_sha1: "Keyed SHA1", + meticulous_keyed_sha1: "Meticulous Keyed SHA1", + } + + def __init__(self, value): + NumericConstant.__init__(self, value) + + +def bfd_is_auth_used(pkt): + """ is packet authenticated? """ + return "A" in pkt.sprintf("%BFD.flags%") + + +def bfd_is_simple_pwd_used(pkt): + """ is simple password authentication used? """ + return bfd_is_auth_used(pkt) and pkt.auth_type == BFDAuthType.simple_pwd + + +def bfd_is_sha1_used(pkt): + """ is sha1 authentication used? """ + return bfd_is_auth_used(pkt) and pkt.auth_type in \ + (BFDAuthType.keyed_sha1, BFDAuthType.meticulous_keyed_sha1) + + +def bfd_is_md5_used(pkt): + """ is md5 authentication used? """ + return bfd_is_auth_used(pkt) and pkt.auth_type in \ + (BFDAuthType.keyed_md5, BFDAuthType.meticulous_keyed_md5) + + +def bfd_is_md5_or_sha1_used(pkt): + """ is md5 or sha1 used? """ + return bfd_is_md5_used(pkt) or bfd_is_sha1_used(pkt) + + +class BFD(Packet): + """ BFD protocol layer for scapy """ + + udp_dport = 3784 #: BFD destination port per RFC 5881 + udp_dport_echo = 3785 # : BFD destination port for ECHO per RFC 5881 + udp_sport_min = 49152 #: BFD source port min value per RFC 5881 + udp_sport_max = 65535 #: BFD source port max value per RFC 5881 + bfd_pkt_len = 24 # : length of BFD pkt without authentication section + sha1_auth_len = 28 # : length of authentication section if SHA1 used + + name = "BFD" + + fields_desc = [ + BitField("version", 1, 3), + BitEnumField("diag", 0, 5, BFDDiagCode.desc_dict), + BitEnumField("state", 0, 2, BFDState.desc_dict), + FlagsField("flags", 0, 6, ['M', 'D', 'A', 'C', 'F', 'P']), + XByteField("detect_mult", 0), + BitField("length", bfd_pkt_len, 8), + BitField("my_discriminator", 0, 32), + BitField("your_discriminator", 0, 32), + BitField("desired_min_tx_interval", 0, 32), + BitField("required_min_rx_interval", 0, 32), + BitField("required_min_echo_rx_interval", 0, 32), + ConditionalField( + BitEnumField("auth_type", 0, 8, BFDAuthType.desc_dict), + bfd_is_auth_used), + ConditionalField(BitField("auth_len", 0, 8), bfd_is_auth_used), + ConditionalField(BitField("auth_key_id", 0, 8), bfd_is_auth_used), + ConditionalField(BitField("auth_reserved", 0, 8), + bfd_is_md5_or_sha1_used), + ConditionalField( + BitField("auth_seq_num", 0, 32), bfd_is_md5_or_sha1_used), + ConditionalField(StrField("auth_key_hash", "0" * 16), bfd_is_md5_used), + ConditionalField( + StrField("auth_key_hash", "0" * 20), bfd_is_sha1_used), + ] + + def mysummary(self): + return self.sprintf("BFD(my_disc=%BFD.my_discriminator%," + "your_disc=%BFD.your_discriminator%)") + +# glue the BFD packet class to scapy parser +bind_layers(UDP, BFD, dport=BFD.udp_dport) + + +class BFD_vpp_echo(Packet): + """ BFD echo packet as used by VPP (non-rfc, as rfc doesn't define one) """ + + udp_dport = 3785 #: BFD echo destination port per RFC 5881 + name = "BFD_VPP_ECHO" + + fields_desc = [ + BitField("discriminator", 0, 32), + BitField("expire_time_clocks", 0, 64), + BitField("checksum", 0, 64) + ] + + def mysummary(self): + return self.sprintf( + "BFD_VPP_ECHO(disc=%BFD_VPP_ECHO.discriminator%," + "expire_time_clocks=%BFD_VPP_ECHO.expire_time_clocks%)") + +# glue the BFD echo packet class to scapy parser +bind_layers(UDP, BFD_vpp_echo, dport=BFD_vpp_echo.udp_dport) + + +class VppBFDAuthKey(VppObject): + """ Represents BFD authentication key in VPP """ + + def __init__(self, test, conf_key_id, auth_type, key): + self._test = test + self._key = key + self._auth_type = auth_type + test.assertIn(auth_type, BFDAuthType.desc_dict) + self._conf_key_id = conf_key_id + + @property + def test(self): + """ Test which created this key """ + return self._test + + @property + def auth_type(self): + """ Authentication type for this key """ + return self._auth_type + + @property + def key(self): + """ key data """ + return self._key + + @key.setter + def key(self, value): + self._key = value + + @property + def conf_key_id(self): + """ configuration key ID """ + return self._conf_key_id + + def add_vpp_config(self): + self.test.vapi.bfd_auth_set_key( + self._conf_key_id, self._auth_type, self._key) + self._test.registry.register(self, self.test.logger) + + def get_bfd_auth_keys_dump_entry(self): + """ get the entry in the auth keys dump corresponding to this key """ + result = self.test.vapi.bfd_auth_keys_dump() + for k in result: + if k.conf_key_id == self._conf_key_id: + return k + return None + + def query_vpp_config(self): + return self.get_bfd_auth_keys_dump_entry() is not None + + def remove_vpp_config(self): + self.test.vapi.bfd_auth_del_key(self._conf_key_id) + + def object_id(self): + return "bfd-auth-key-%s" % self._conf_key_id + + def __str__(self): + return self.object_id() + + +class VppBFDUDPSession(VppObject): + """ Represents BFD UDP session in VPP """ + + def __init__(self, test, interface, peer_addr, local_addr=None, af=AF_INET, + desired_min_tx=300000, required_min_rx=300000, detect_mult=3, + sha1_key=None, bfd_key_id=None): + self._test = test + self._interface = interface + self._af = af + self._local_addr = local_addr + if local_addr is not None: + self._local_addr_n = inet_pton(af, local_addr) + else: + self._local_addr_n = None + self._peer_addr = peer_addr + self._peer_addr_n = inet_pton(af, peer_addr) + self._desired_min_tx = desired_min_tx + self._required_min_rx = required_min_rx + self._detect_mult = detect_mult + self._sha1_key = sha1_key + if bfd_key_id is not None: + self._bfd_key_id = bfd_key_id + else: + self._bfd_key_id = randint(0, 255) + + @property + def test(self): + """ Test which created this session """ + return self._test + + @property + def interface(self): + """ Interface on which this session lives """ + return self._interface + + @property + def af(self): + """ Address family - AF_INET or AF_INET6 """ + return self._af + + @property + def local_addr(self): + """ BFD session local address (VPP address) """ + if self._local_addr is None: + if self.af == AF_INET: + return self._interface.local_ip4 + elif self.af == AF_INET6: + return self._interface.local_ip6 + else: + raise Exception("Unexpected af '%s'" % self.af) + return self._local_addr + + @property + def local_addr_n(self): + """ BFD session local address (VPP address) - raw, suitable for API """ + if self._local_addr is None: + if self.af == AF_INET: + return self._interface.local_ip4n + elif self.af == AF_INET6: + return self._interface.local_ip6n + else: + raise Exception("Unexpected af '%s'" % self.af) + return self._local_addr_n + + @property + def peer_addr(self): + """ BFD session peer address """ + return self._peer_addr + + @property + def peer_addr_n(self): + """ BFD session peer address - raw, suitable for API """ + return self._peer_addr_n + + def get_bfd_udp_session_dump_entry(self): + """ get the namedtuple entry from bfd udp session dump """ + result = self.test.vapi.bfd_udp_session_dump() + for s in result: + self.test.logger.debug("session entry: %s" % str(s)) + if s.sw_if_index == self.interface.sw_if_index: + if self.af == AF_INET \ + and s.is_ipv6 == 0 \ + and self.interface.local_ip4n == s.local_addr[:4] \ + and self.interface.remote_ip4n == s.peer_addr[:4]: + return s + if self.af == AF_INET6 \ + and s.is_ipv6 == 1 \ + and self.interface.local_ip6n == s.local_addr \ + and self.interface.remote_ip6n == s.peer_addr: + return s + return None + + @property + def state(self): + """ BFD session state """ + session = self.get_bfd_udp_session_dump_entry() + if session is None: + raise Exception("Could not find BFD session in VPP response") + return session.state + + @property + def desired_min_tx(self): + """ desired minimum tx interval """ + return self._desired_min_tx + + @property + def required_min_rx(self): + """ required minimum rx interval """ + return self._required_min_rx + + @property + def detect_mult(self): + """ detect multiplier """ + return self._detect_mult + + @property + def sha1_key(self): + """ sha1 key """ + return self._sha1_key + + @property + def bfd_key_id(self): + """ bfd key id in use """ + return self._bfd_key_id + + def activate_auth(self, key, bfd_key_id=None, delayed=False): + """ activate authentication for this session """ + self._bfd_key_id = bfd_key_id if bfd_key_id else randint(0, 255) + self._sha1_key = key + is_ipv6 = 1 if AF_INET6 == self.af else 0 + conf_key_id = self._sha1_key.conf_key_id + is_delayed = 1 if delayed else 0 + self.test.vapi.bfd_udp_auth_activate(self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6, + bfd_key_id=self._bfd_key_id, + conf_key_id=conf_key_id, + is_delayed=is_delayed) + + def deactivate_auth(self, delayed=False): + """ deactivate authentication """ + self._bfd_key_id = None + self._sha1_key = None + is_delayed = 1 if delayed else 0 + is_ipv6 = 1 if AF_INET6 == self.af else 0 + self.test.vapi.bfd_udp_auth_deactivate(self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6, + is_delayed=is_delayed) + + def modify_parameters(self, + detect_mult=None, + desired_min_tx=None, + required_min_rx=None): + """ modify session parameters """ + if detect_mult: + self._detect_mult = detect_mult + if desired_min_tx: + self._desired_min_tx = desired_min_tx + if required_min_rx: + self._required_min_rx = required_min_rx + is_ipv6 = 1 if AF_INET6 == self.af else 0 + self.test.vapi.bfd_udp_mod(self._interface.sw_if_index, + self.desired_min_tx, + self.required_min_rx, + self.detect_mult, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + + def add_vpp_config(self): + is_ipv6 = 1 if AF_INET6 == self.af else 0 + bfd_key_id = self._bfd_key_id if self._sha1_key else None + conf_key_id = self._sha1_key.conf_key_id if self._sha1_key else None + self.test.vapi.bfd_udp_add(self._interface.sw_if_index, + self.desired_min_tx, + self.required_min_rx, + self.detect_mult, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6, + bfd_key_id=bfd_key_id, + conf_key_id=conf_key_id) + self._test.registry.register(self, self.test.logger) + + def query_vpp_config(self): + session = self.get_bfd_udp_session_dump_entry() + return session is not None + + def remove_vpp_config(self): + is_ipv6 = 1 if AF_INET6 == self._af else 0 + self.test.vapi.bfd_udp_del(self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + + def object_id(self): + return "bfd-udp-%s-%s-%s-%s" % (self._interface.sw_if_index, + self.local_addr, + self.peer_addr, + self.af) + + def __str__(self): + return self.object_id() + + def admin_up(self): + """ set bfd session admin-up """ + is_ipv6 = 1 if AF_INET6 == self._af else 0 + self.test.vapi.bfd_udp_session_set_flags(1, + self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) + + def admin_down(self): + """ set bfd session admin-down """ + is_ipv6 = 1 if AF_INET6 == self._af else 0 + self.test.vapi.bfd_udp_session_set_flags(0, + self._interface.sw_if_index, + self.local_addr_n, + self.peer_addr_n, + is_ipv6=is_ipv6) diff --git a/test/debug.py b/test/debug.py new file mode 100644 index 00000000..4516b8c1 --- /dev/null +++ b/test/debug.py @@ -0,0 +1,23 @@ +""" debug utilities """ + +import os +import pexpect + +gdb_path = '/usr/bin/gdb' + + +def spawn_gdb(binary_path, core_path, logger): + if os.path.isfile(gdb_path) and os.access(gdb_path, os.X_OK): + # automatically attach gdb + gdb_cmdline = "%s %s %s" % (gdb_path, binary_path, core_path) + gdb = pexpect.spawn(gdb_cmdline) + gdb.interact() + try: + gdb.terminate(True) + except: + pass + if gdb.isalive(): + raise Exception("GDB refused to die...") + else: + logger.error("Debugger '%s' does not exist or is not an " + "executable.." % gdb_path) diff --git a/test/discover_tests.py b/test/discover_tests.py new file mode 100755 index 00000000..eea59410 --- /dev/null +++ b/test/discover_tests.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +import sys +import os +import unittest +import importlib +import argparse + + +def discover_tests(directory, callback): + do_insert = True + for _f in os.listdir(directory): + f = "%s/%s" % (directory, _f) + if os.path.isdir(f): + discover_tests(f, callback) + continue + if not os.path.isfile(f): + continue + if do_insert: + sys.path.insert(0, directory) + do_insert = False + if not _f.startswith("test_") or not _f.endswith(".py"): + continue + name = "".join(f.split("/")[-1].split(".")[:-1]) + if name in sys.modules: + raise Exception("Duplicate test module `%s' found!" % name) + module = importlib.import_module(name) + for name, cls in module.__dict__.items(): + if not isinstance(cls, type): + continue + if not issubclass(cls, unittest.TestCase): + continue + if name == "VppTestCase": + continue + for method in dir(cls): + if not callable(getattr(cls, method)): + continue + if method.startswith("test_"): + callback(_f, cls, method) + + +def print_callback(file_name, cls, method): + print("%s.%s.%s" % (file_name, cls.__name__, method)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Discover VPP unit tests") + parser.add_argument("-d", "--dir", action='append', type=str, + help="directory containing test files " + "(may be specified multiple times)") + args = parser.parse_args() + if args.dir is None: + args.dir = "." + + suite = unittest.TestSuite() + for d in args.dir: + discover_tests(d, print_callback) diff --git a/test/doc/Makefile b/test/doc/Makefile new file mode 100644 index 00000000..ff96b5fd --- /dev/null +++ b/test/doc/Makefile @@ -0,0 +1,243 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SRC_DOC_DIR = $(WS_ROOT)/test/doc +SPHINXBUILD = sphinx-build +PAPER = +BUILD_DOC_ROOT = $(BR)/test-doc +BUILD_DOC_DIR = $(BUILD_DOC_ROOT)/build +API_DOC_GEN_DIR = $(BUILD_DOC_ROOT)/apidoc + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILD_DOC_DIR)/.sphinx-cache $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(API_DOC_GEN_DIR) -c $(SRC_DOC_DIR) +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +IN_VENV:=$(shell if pip -V | grep "virtualenv" 2>&1 > /dev/null; then echo 1; else echo 0; fi) + +.PHONY: verify-virtualenv +verify-virtualenv: +ifeq ($(IN_VENV),0) + $(error "Not running inside virtualenv (are you running 'make test-doc' from root?)") +endif + +.PHONY: regen-api-doc +regen-api-doc: verify-virtualenv + @mkdir -p $(API_DOC_GEN_DIR) + @cp $(SRC_DOC_DIR)/index.rst $(API_DOC_GEN_DIR) + @cp $(SRC_DOC_DIR)/indices.rst $(API_DOC_GEN_DIR) + @cp $(SRC_DOC_DIR)/overview.rst $(API_DOC_GEN_DIR) + sphinx-apidoc -o $(API_DOC_GEN_DIR) -H "Module documentation" .. + +.PHONY: help +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: wipe +wipe: + rm -rf $(BUILD_DOC_ROOT) + +.PHONY: html +html: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/html." + +.PHONY: dirhtml +dirhtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILD_DOC_DIR)/singlehtml." + +.PHONY: pickle +pickle: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILD_DOC_DIR)/htmlhelp." + +.PHONY: qthelp +qthelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILD_DOC_DIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhc" + +.PHONY: applehelp +applehelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILD_DOC_DIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/VPPtestframework" + @echo "# ln -s $(BUILD_DOC_DIR)/devhelp $$HOME/.local/share/devhelp/VPPtestframework" + @echo "# devhelp" + +.PHONY: epub +epub: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILD_DOC_DIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILD_DOC_DIR)/epub3." + +.PHONY: latex +latex: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILD_DOC_DIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." + +.PHONY: latexpdfja +latexpdfja: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." + +.PHONY: text +text: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/text + @echo + @echo "Build finished. The text files are in $(BUILD_DOC_DIR)/text." + +.PHONY: man +man: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILD_DOC_DIR)/man." + +.PHONY: texinfo +texinfo: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILD_DOC_DIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILD_DOC_DIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILD_DOC_DIR)/texinfo." + +.PHONY: gettext +gettext: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILD_DOC_DIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILD_DOC_DIR)/locale." + +.PHONY: changes +changes: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/changes + @echo + @echo "The overview file is in $(BUILD_DOC_DIR)/changes." + +.PHONY: linkcheck +linkcheck: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILD_DOC_DIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILD_DOC_DIR)/doctest/output.txt." + +.PHONY: coverage +coverage: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILD_DOC_DIR)/coverage/python.txt." + +.PHONY: xml +xml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILD_DOC_DIR)/xml." + +.PHONY: pseudoxml +pseudoxml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILD_DOC_DIR)/pseudoxml." + +.PHONY: dummy +dummy: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/test/doc/conf.py b/test/doc/conf.py new file mode 100644 index 00000000..ec8958ea --- /dev/null +++ b/test/doc/conf.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +# +# VPP test framework documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 13 08:45:03 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'VPP test framework' +copyright = u'2016, VPP team' +author = u'VPP team' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +default_role = 'any' + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# "<project> v<release> documentation" by default. +# +# html_title = u'VPP test framework v0.1' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = [] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'VPPtestframeworkdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'VPPtestframework.tex', u'VPP test framework Documentation', + u'VPP team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'vpptestframework', u'VPP test framework Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'VPPtestframework', u'VPP test framework Documentation', + author, 'VPPtestframework', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/test/doc/index.rst b/test/doc/index.rst new file mode 100644 index 00000000..62e348cd --- /dev/null +++ b/test/doc/index.rst @@ -0,0 +1,11 @@ +Contents +======== + +.. toctree:: + :numbered: + :maxdepth: 2 + :glob: + + overview + modules + indices diff --git a/test/doc/indices.rst b/test/doc/indices.rst new file mode 100644 index 00000000..d46b839f --- /dev/null +++ b/test/doc/indices.rst @@ -0,0 +1,6 @@ +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/test/doc/overview.rst b/test/doc/overview.rst new file mode 100644 index 00000000..7b70eded --- /dev/null +++ b/test/doc/overview.rst @@ -0,0 +1,418 @@ +.. _unittest: https://docs.python.org/2/library/unittest.html +.. _TestCase: https://docs.python.org/2/library/unittest.html#unittest.TestCase +.. _AssertionError: https://docs.python.org/2/library/exceptions.html#exceptions.AssertionError +.. _SkipTest: https://docs.python.org/2/library/unittest.html#unittest.SkipTest +.. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/ +.. _scapy: http://www.secdev.org/projects/scapy/ +.. _logging: https://docs.python.org/2/library/logging.html + +.. |vtf| replace:: VPP Test Framework + +|vtf| +===== + +.. contents:: + :local: + :depth: 1 + +Overview +######## + +The goal of the |vtf| is to ease writing, running and debugging +unit tests for the VPP. For this, python was chosen as a high level language +allowing rapid development with scapy_ providing the necessary tool for creating +and dissecting packets. + +Anatomy of a test case +###################### + +Python's unittest_ is used as the base framework upon which the VPP test +framework is built. A test suite in the |vtf| consists of multiple classes +derived from `VppTestCase`, which is itself derived from TestCase_. +The test class defines one or more test functions, which act as test cases. + +Function flow when running a test case is: + +1. `setUpClass <VppTestCase.setUpClass>`: + This function is called once for each test class, allowing a one-time test + setup to be executed. If this functions throws an exception, + none of the test functions are executed. +2. `setUp <VppTestCase.setUp>`: + The setUp function runs before each of the test functions. If this function + throws an exception other than AssertionError_ or SkipTest_, then this is + considered an error, not a test failure. +3. *test_<name>*: + This is the guts of the test case. It should execute the test scenario + and use the various assert functions from the unittest framework to check + necessary. Multiple test_<name> methods can exist in a test case. +4. `tearDown <VppTestCase.tearDown>`: + The tearDown function is called after each test function with the purpose + of doing partial cleanup. +5. `tearDownClass <VppTestCase.tearDownClass>`: + Method called once after running all of the test functions to perform + the final cleanup. + +Logging +####### + +Each test case has a logger automatically created for it, stored in +'logger' property, based on logging_. Use the logger's standard methods +debug(), info(), error(), ... to emit log messages to the logger. + +All the log messages go always into a log file in temporary directory +(see below). + +To control the messages printed to console, specify the V= parameter. + +.. code-block:: shell + + make test # minimum verbosity + make test V=1 # moderate verbosity + make test V=2 # maximum verbosity + +Test temporary directory and VPP life cycle +########################################### + +Test separation is achieved by separating the test files and vpp instances. +Each test creates a temporary directory and it's name is used to create +a shared memory prefix which is used to run a VPP instance. +The temporary directory name contains the testcase class name for easy +reference, so for testcase named 'TestVxlan' the directory could be named +e.g. vpp-unittest-TestVxlan-UNUP3j. +This way, there is no conflict between any other VPP instances running +on the box and the test VPP. Any temporary files created by the test case +are stored in this temporary test directory. + +The test temporary directory holds the following interesting files: + +* log.txt - this contains the logger output on max verbosity +* pg*_in.pcap - last injected packet stream into VPP, named after the interface, + so for pg0, the file will be named pg0_in.pcap +* pg*_out.pcap - last capture file created by VPP for interface, similarly, + named after the interface, so for e.g. pg1, the file will be named + pg1_out.pcap +* history files - whenever the capture is restarted or a new stream is added, + the existing files are rotated and renamed, soo all the pcap files + are always saved for later debugging if needed +* core - if vpp dumps a core, it'll be stored in the temporary directory +* vpp_stdout.txt - file containing output which vpp printed to stdout +* vpp_stderr.txt - file containing output which vpp printed to stderr + +*NOTE*: existing temporary directories named vpp-unittest-* are automatically +removed when invoking 'make test*' or 'make retest*' to keep the temporary +directory clean. + +Virtual environment +################### + +Virtualenv_ is a python module which provides a means to create an environment +containing the dependencies required by the |vtf|, allowing a separation +from any existing system-wide packages. |vtf|'s Makefile automatically +creates a virtualenv_ inside build-root and installs the required packages +in that environment. The environment is entered whenever executing a test +via one of the make test targets. + +Naming conventions +################## + +Most unit tests do some kind of packet manipulation - sending and receiving +packets between VPP and virtual hosts connected to the VPP. Referring +to the sides, addresses, etc. is always done as if looking from the VPP side, +thus: + +* *local_* prefix is used for the VPP side. + So e.g. `local_ip4 <VppInterface.local_ip4>` address is the IPv4 address + assigned to the VPP interface. +* *remote_* prefix is used for the virtual host side. + So e.g. `remote_mac <VppInterface.remote_mac>` address is the MAC address + assigned to the virtual host connected to the VPP. + +Automatically generated addresses +################################# + +To send packets, one needs to typically provide some addresses, otherwise +the packets will be dropped. The interface objects in |vtf| automatically +provide addresses based on (typically) their indexes, which ensures +there are no conflicts and eases debugging by making the addressing scheme +consistent. + +The developer of a test case typically doesn't need to work with the actual +numbers, rather using the properties of the objects. The addresses typically +come in two flavors: '<address>' and '<address>n' - note the 'n' suffix. +The former address is a Python string, while the latter is translated using +socket.inet_pton to raw format in network byte order - this format is suitable +for passing as an argument to VPP APIs. + +e.g. for the IPv4 address assigned to the VPP interface: + +* local_ip4 - Local IPv4 address on VPP interface (string) +* local_ip4n - Local IPv4 address - raw, suitable as API parameter. + +These addresses need to be configured in VPP to be usable using e.g. +`config_ip4` API. Please see the documentation to `VppInterface` for more +details. + +By default, there is one remote address of each kind created for L3: +remote_ip4 and remote_ip6. If the test needs more addresses, because it's +simulating more remote hosts, they can be generated using +`generate_remote_hosts` API and the entries for them inserted into the ARP +table using `configure_ipv4_neighbors` API. + +Packet flow in the |vtf| +######################## + +Test framework -> VPP +~~~~~~~~~~~~~~~~~~~~~ + +|vtf| doesn't send any packets to VPP directly. Traffic is instead injected +using packet-generator interfaces, represented by the `VppPGInterface` class. +Packets are written into a temporary .pcap file, which is then read by the VPP +and the packets are injected into the VPP world. + +To add a list of packets to an interface, call the `add_stream` method on that +interface. Once everything is prepared, call `pg_start` method to start +the packet generator on the VPP side. + +VPP -> test framework +~~~~~~~~~~~~~~~~~~~~~ + +Similarly, VPP doesn't send any packets to |vtf| directly. Instead, packet +capture feature is used to capture and write traffic to a temporary .pcap file, +which is then read and analyzed by the |vtf|. + +The following APIs are available to the test case for reading pcap files. + +* `get_capture`: this API is suitable for bulk & batch style of test, where + a list of packets is prepared & sent, then the received packets are read + and verified. The API needs the number of packets which are expected to + be captured (ignoring filtered packets - see below) to know when the pcap + file is completely written by the VPP. If using packet infos for verifying + packets, then the counts of the packet infos can be automatically used + by `get_capture` to get the proper count (in this case the default value + None can be supplied as expected_count or ommitted altogether). +* `wait_for_packet`: this API is suitable for interactive style of test, + e.g. when doing session management, three-way handsakes, etc. This API waits + for and returns a single packet, keeping the capture file in place + and remembering context. Repeated invocations return following packets + (or raise Exception if timeout is reached) from the same capture file + (= packets arriving on the same interface). + +*NOTE*: it is not recommended to mix these APIs unless you understand how they +work internally. None of these APIs rotate the pcap capture file, so calling +e.g. `get_capture` after `wait_for_packet` will return already read packets. +It is safe to switch from one API to another after calling `enable_capture` +as that API rotates the capture file. + +Automatic filtering of packets: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both APIs (`get_capture` and `wait_for_packet`) by default filter the packet +capture, removing known uninteresting packets from it - these are IPv6 Router +Advertisments and IPv6 Router Alerts. These packets are unsolicitated +and from the point of |vtf| are random. If a test wants to receive these +packets, it should specify either None or a custom filtering function +as the value to the 'filter_out_fn' argument. + +Common API flow for sending/receiving packets: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We will describe a simple scenario, where packets are sent from pg0 to pg1 +interface, assuming that the interfaces were created using +`create_pg_interfaces` API. + +1. Create a list of packets for pg0:: + + packet_count = 10 + packets = create_packets(src=self.pg0, dst=self.pg1, + count=packet_count) + +2. Add that list of packets to the source interface:: + + self.pg0.add_stream(packets) + +3. Enable capture on the destination interface:: + + self.pg1.enable_capture() + +4. Start the packet generator:: + + self.pg_start() + +5. Wait for capture file to appear and read it:: + + capture = self.pg1.get_capture(expected_count=packet_count) + +6. Verify packets match sent packets:: + + self.verify_capture(send=packets, captured=capture) + +Test framework objects +###################### + +The following objects provide VPP abstraction and provide a means to do +common tasks easily in the test cases. + +* `VppInterface`: abstract class representing generic VPP interface + and contains some common functionality, which is then used by derived classes +* `VppPGInterface`: class representing VPP packet-generator interface. + The interface is created/destroyed when the object is created/destroyed. +* `VppSubInterface`: VPP sub-interface abstract class, containing common + functionality for e.g. `VppDot1QSubint` and `VppDot1ADSubint` classes + +How VPP APIs/CLIs are called +############################ + +Vpp provides python bindings in a python module called vpp-papi, which the test +framework installs in the virtual environment. A shim layer represented by +the `VppPapiProvider` class is built on top of the vpp-papi, serving these +purposes: + +1. Automatic return value checks: + After each API is called, the return value is checked against the expected + return value (by default 0, but can be overridden) and an exception + is raised if the check fails. +2. Automatic call of hooks: + + a. `before_cli <Hook.before_cli>` and `before_api <Hook.before_api>` hooks + are used for debug logging and stepping through the test + b. `after_cli <Hook.after_cli>` and `after_api <Hook.after_api>` hooks + are used for monitoring the vpp process for crashes +3. Simplification of API calls: + Many of the VPP APIs take a lot of parameters and by providing sane defaults + for these, the API is much easier to use in the common case and the code is + more readable. E.g. ip_add_del_route API takes ~25 parameters, of which + in the common case, only 3 are needed. + +Utility methods +############### + +Some interesting utility methods are: + +* `ppp`: 'Pretty Print Packet' - returns a string containing the same output + as Scapy's packet.show() would print +* `ppc`: 'Pretty Print Capture' - returns a string containing printout of + a capture (with configurable limit on the number of packets printed from it) + using `ppp` + +*NOTE*: Do not use Scapy's packet.show() in the tests, because it prints +the output to stdout. All output should go to the logger associated with +the test case. + +Example: how to add a new test +############################## + +In this example, we will describe how to add a new test case which tests +basic IPv4 forwarding. + +1. Add a new file called test_ip4_fwd.py in the test directory, starting + with a few imports:: + + from framework import VppTestCase + from scapy.layers.l2 import Ether + from scapy.packet import Raw + from scapy.layers.inet import IP, UDP + from random import randint + +2. Create a class inherited from the VppTestCase:: + + class IP4FwdTestCase(VppTestCase): + """ IPv4 simple forwarding test case """ + +2. Add a setUpClass function containing the setup needed for our test to run:: + + @classmethod + def setUpClass(self): + super(IP4FwdTestCase, self).setUpClass() + self.create_pg_interfaces(range(2)) # create pg0 and pg1 + for i in self.pg_interfaces: + i.admin_up() # put the interface up + i.config_ip4() # configure IPv4 address on the interface + i.resolve_arp() # resolve ARP, so that we know VPP MAC + +3. Create a helper method to create the packets to send:: + + def create_stream(self, src_if, dst_if, count): + packets = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / + UDP(sport=randint(1000, 2000), dport=5678) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + packets.append(p) + + # return the created packet list + return packets + +4. Create a helper method to verify the capture:: + + def verify_capture(self, src_if, dst_if, capture): + packet_info = None + for packet in capture: + try: + ip = packet[IP] + udp = packet[UDP] + # convert the payload to packet info object + payload_info = self.payload_to_info(str(packet[Raw])) + # make sure the indexes match + self.assert_equal(payload_info.src, src_if.sw_if_index, + "source sw_if_index") + self.assert_equal(payload_info.dst, dst_if.sw_if_index, + "destination sw_if_index") + packet_info = self.get_next_packet_info_for_interface2( + src_if.sw_if_index, + dst_if.sw_if_index, + packet_info) + # make sure we didn't run out of saved packets + self.assertIsNotNone(packet_info) + self.assert_equal(payload_info.index, packet_info.index, + "packet info index") + saved_packet = packet_info.data # fetch the saved packet + # assert the values match + self.assert_equal(ip.src, saved_packet[IP].src, + "IP source address") + # ... more assertions here + self.assert_equal(udp.sport, saved_packet[UDP].sport, + "UDP source port") + except: + self.logger.error(ppp("Unexpected or invalid packet:", + packet)) + raise + remaining_packet = self.get_next_packet_info_for_interface2( + src_if.sw_if_index, + dst_if.sw_if_index, + packet_info) + self.assertIsNone(remaining_packet, + "Interface %s: Packet expected from interface " + "%s didn't arrive" % (dst_if.name, src_if.name)) + +5. Add the test code to test_basic function:: + + def test_basic(self): + count = 10 + # create the packet stream + packets = self.create_stream(self.pg0, self.pg1, count) + # add the stream to the source interface + self.pg0.add_stream(packets) + # enable capture on both interfaces + self.pg0.enable_capture() + self.pg1.enable_capture() + # start the packet generator + self.pg_start() + # get capture - the proper count of packets was saved by + # create_packet_info() based on dst_if parameter + capture = self.pg1.get_capture() + # assert nothing captured on pg0 (always do this last, so that + # some time has already passed since pg_start()) + self.pg0.assert_nothing_captured() + # verify capture + self.verify_capture(self.pg0, self.pg1, capture) + +6. Run the test by issuing 'make test'. diff --git a/test/ext/Makefile b/test/ext/Makefile new file mode 100644 index 00000000..6b3cb907 --- /dev/null +++ b/test/ext/Makefile @@ -0,0 +1,31 @@ +BINDIR = $(BR)/vapi_test/ +CBIN = $(addprefix $(BINDIR), vapi_c_test) +CPPBIN = $(addprefix $(BINDIR), vapi_cpp_test) + +LIBS = -L$(VPP_TEST_BUILD_DIR)/vpp/.libs/ -L$(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi/.libs/ -lvppinfra -lvlibmemoryclient -lsvm -lpthread -lcheck -lsubunit -lrt -lm -lvapiclient +CFLAGS = -std=gnu99 -g -Wall -pthread -I$(WS_ROOT)/src -I$(VPP_TEST_INSTALL_PATH)/vpp/include -I$(BINDIR) +CPPFLAGS = -std=c++11 -g -Wall -pthread -I$(WS_ROOT)/src -I$(VPP_TEST_INSTALL_PATH)/vpp/include -I$(BINDIR) + +all: $(CBIN) $(CPPBIN) + +$(BINDIR): + mkdir -p $(BINDIR) + +CSRC = vapi_c_test.c + +$(BINDIR)/fake.api.vapi.h: fake.api.json $(WS_ROOT)/src/vpp-api/vapi/vapi_c_gen.py | $(BINDIR) + $(WS_ROOT)/src/vpp-api/vapi/vapi_c_gen.py --prefix $(BINDIR) $< + +$(BINDIR)/fake.api.vapi.hpp: fake.api.json $(WS_ROOT)/src/vpp-api/vapi/vapi_cpp_gen.py | $(BINDIR) + $(WS_ROOT)/src/vpp-api/vapi/vapi_cpp_gen.py --prefix $(BINDIR) $< + +$(CBIN): $(CSRC) $(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi/.libs/libvapiclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvppinfra.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvlibmemoryclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libsvm.so $(BINDIR)/fake.api.vapi.h + $(CC) -o $@ $(CFLAGS) $(CSRC) $(LIBS) + +CPPSRC = vapi_cpp_test.cpp + +$(CPPBIN): $(CPPSRC) $(VPP_TEST_BUILD_DIR)/vpp/vpp-api/vapi/.libs/libvapiclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvppinfra.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libvlibmemoryclient.so $(VPP_TEST_BUILD_DIR)/vpp/.libs/libsvm.so $(BINDIR)/fake.api.vapi.hpp + $(CXX) -o $@ $(CPPFLAGS) $(CPPSRC) $(LIBS) + +clean: + rm -rf $(BINDIR) diff --git a/test/ext/fake.api.json b/test/ext/fake.api.json new file mode 100644 index 00000000..3e8d6a95 --- /dev/null +++ b/test/ext/fake.api.json @@ -0,0 +1,35 @@ +{ + "types" : [ + + ], + "messages" : [ + ["test_fake_msg", + ["u16", "_vl_msg_id"], + ["u32", "client_index"], + ["u32", "context"], + ["u8", "dummy", 256], + {"crc" : "0xcafebafe"} + ], + ["test_fake_msg_reply", + ["u16", "_vl_msg_id"], + ["u32", "context"], + ["i32", "retval"], + {"crc" : "0xcafebafe"} + ], + ["test_fake_dump", + ["u16", "_vl_msg_id"], + ["u32", "client_index"], + ["u32", "context"], + ["u32", "dummy"], + {"crc" : "0xcafebafe"} + ], + ["test_fake_details", + ["u16", "_vl_msg_id"], + ["u32", "client_index"], + ["u32", "context"], + ["u32", "dummy"], + {"crc" : "0xcafebafe"} + ] + ], +"vl_api_version" :"0x224c7aad" +} diff --git a/test/ext/vapi_c_test.c b/test/ext/vapi_c_test.c new file mode 100644 index 00000000..622b617b --- /dev/null +++ b/test/ext/vapi_c_test.c @@ -0,0 +1,1168 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 Cisco and/or its affiliates. + * 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. + *------------------------------------------------------------------ + */ + +#include <stdio.h> +#include <endian.h> +#include <stdlib.h> +#include <unistd.h> +#include <assert.h> +#include <setjmp.h> +#include <check.h> +#include <vapi/vapi.h> +#include <vapi/vpe.api.vapi.h> +#include <vapi/interface.api.vapi.h> +#include <vapi/l2.api.vapi.h> +#include <vapi/stats.api.vapi.h> +#include <fake.api.vapi.h> + +DEFINE_VAPI_MSG_IDS_VPE_API_JSON; +DEFINE_VAPI_MSG_IDS_INTERFACE_API_JSON; +DEFINE_VAPI_MSG_IDS_L2_API_JSON; +DEFINE_VAPI_MSG_IDS_STATS_API_JSON; +DEFINE_VAPI_MSG_IDS_FAKE_API_JSON; + +static char *app_name = NULL; +static char *api_prefix = NULL; +static const int max_outstanding_requests = 64; +static const int response_queue_size = 32; + +START_TEST (test_invalid_values) +{ + vapi_ctx_t ctx; + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_eq (NULL, sv); + rv = vapi_send (ctx, sv); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_send (ctx, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_send (NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_recv (NULL, NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_recv (ctx, NULL, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + vapi_msg_show_version_reply *reply; + rv = vapi_recv (ctx, (void **) &reply, NULL); + ck_assert_int_eq (VAPI_EINVAL, rv); + rv = vapi_disconnect (ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_ctx_free (ctx); +} + +END_TEST; + +START_TEST (test_hton_1) +{ + const u16 _vl_msg_id = 1; + vapi_type_msg_header1_t h; + h._vl_msg_id = _vl_msg_id; + vapi_type_msg_header1_t_hton (&h); + ck_assert_int_eq (be16toh (h._vl_msg_id), _vl_msg_id); +} + +END_TEST; + +START_TEST (test_hton_2) +{ + const u16 _vl_msg_id = 1; + const u32 client_index = 3; + vapi_type_msg_header2_t h; + h._vl_msg_id = _vl_msg_id; + h.client_index = client_index; + vapi_type_msg_header2_t_hton (&h); + ck_assert_int_eq (be16toh (h._vl_msg_id), _vl_msg_id); + ck_assert_int_eq (h.client_index, client_index); +} + +END_TEST; + +START_TEST (test_hton_3) +{ + const size_t data_size = 10; + vapi_msg_vnet_interface_combined_counters *m = + malloc (sizeof (vapi_msg_vnet_interface_combined_counters) + + data_size * sizeof (vapi_type_vlib_counter)); + ck_assert_ptr_ne (NULL, m); + vapi_payload_vnet_interface_combined_counters *p = &m->payload; + const u16 _vl_msg_id = 1; + p->_vl_msg_id = _vl_msg_id; + const u32 first_sw_if_index = 2; + p->first_sw_if_index = first_sw_if_index; + p->count = data_size; + const u64 packets = 1234; + const u64 bytes = 2345; + int i; + for (i = 0; i < data_size; ++i) + { + p->data[i].packets = packets; + p->data[i].bytes = bytes; + } + vapi_msg_vnet_interface_combined_counters_hton (m); + ck_assert_int_eq (_vl_msg_id, be16toh (p->_vl_msg_id)); + ck_assert_int_eq (first_sw_if_index, be32toh (p->first_sw_if_index)); + ck_assert_int_eq (data_size, be32toh (p->count)); + for (i = 0; i < data_size; ++i) + { + ck_assert_int_eq (packets, be64toh (p->data[i].packets)); + ck_assert_int_eq (bytes, be64toh (p->data[i].bytes)); + } + free (p); +} + +END_TEST; + +#define verify_hton_swap(expr, value) \ + if (4 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, htobe32 (value)); \ + } \ + else if (2 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, htobe16 (value)); \ + } \ + else \ + { \ + ck_assert_int_eq (expr, value); \ + } + +START_TEST (test_hton_4) +{ + const int vla_count = 3; + char x[sizeof (vapi_msg_bridge_domain_details) + + vla_count * sizeof (vapi_type_bridge_domain_sw_if)]; + vapi_msg_bridge_domain_details *d = (void *) x; + int cnt = 1; + d->header._vl_msg_id = cnt++; + d->header.context = cnt++; + d->payload.bd_id = cnt++; + d->payload.flood = cnt++; + d->payload.uu_flood = cnt++; + d->payload.forward = cnt++; + d->payload.learn = cnt++; + d->payload.arp_term = cnt++; + d->payload.mac_age = cnt++; + d->payload.bvi_sw_if_index = cnt++; + d->payload.n_sw_ifs = vla_count; + int i; + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + det->context = cnt++; + det->sw_if_index = cnt++; + det->shg = cnt++; + } + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); + vapi_msg_bridge_domain_details_hton (d); + int tmp = 1; + verify_hton_swap (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + verify_hton_swap (d->payload.bd_id, tmp); + ++tmp; + verify_hton_swap (d->payload.flood, tmp); + ++tmp; + verify_hton_swap (d->payload.uu_flood, tmp); + ++tmp; + verify_hton_swap (d->payload.forward, tmp); + ++tmp; + verify_hton_swap (d->payload.learn, tmp); + ++tmp; + verify_hton_swap (d->payload.arp_term, tmp); + ++tmp; + verify_hton_swap (d->payload.mac_age, tmp); + ++tmp; + verify_hton_swap (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, htobe32 (vla_count)); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + verify_hton_swap (det->context, tmp); + ++tmp; + verify_hton_swap (det->sw_if_index, tmp); + ++tmp; + verify_hton_swap (det->shg, tmp); + ++tmp; + } + vapi_msg_bridge_domain_details_ntoh (d); + tmp = 1; + ck_assert_int_eq (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bd_id, tmp); + ++tmp; + ck_assert_int_eq (d->payload.flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.uu_flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.forward, tmp); + ++tmp; + ck_assert_int_eq (d->payload.learn, tmp); + ++tmp; + ck_assert_int_eq (d->payload.arp_term, tmp); + ++tmp; + ck_assert_int_eq (d->payload.mac_age, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, vla_count); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + ck_assert_int_eq (det->context, tmp); + ++tmp; + ck_assert_int_eq (det->sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (det->shg, tmp); + ++tmp; + } + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); +} + +END_TEST; + +START_TEST (test_ntoh_1) +{ + const u16 _vl_msg_id = 1; + vapi_type_msg_header1_t h; + h._vl_msg_id = _vl_msg_id; + vapi_type_msg_header1_t_ntoh (&h); + ck_assert_int_eq (htobe16 (h._vl_msg_id), _vl_msg_id); +} + +END_TEST; + +START_TEST (test_ntoh_2) +{ + const u16 _vl_msg_id = 1; + const u32 client_index = 3; + vapi_type_msg_header2_t h; + h._vl_msg_id = _vl_msg_id; + h.client_index = client_index; + vapi_type_msg_header2_t_ntoh (&h); + ck_assert_int_eq (htobe16 (h._vl_msg_id), _vl_msg_id); + ck_assert_int_eq (h.client_index, client_index); +} + +END_TEST; + +START_TEST (test_ntoh_3) +{ + const size_t data_size = 10; + vapi_msg_vnet_interface_combined_counters *m = + malloc (sizeof (vapi_msg_vnet_interface_combined_counters) + + data_size * sizeof (vapi_type_vlib_counter)); + ck_assert_ptr_ne (NULL, m); + vapi_payload_vnet_interface_combined_counters *p = &m->payload; + const u16 _vl_msg_id = 1; + p->_vl_msg_id = _vl_msg_id; + const u32 first_sw_if_index = 2; + p->first_sw_if_index = first_sw_if_index; + const size_t be_data_size = htobe32 (data_size); + p->count = be_data_size; + const u64 packets = 1234; + const u64 bytes = 2345; + int i; + for (i = 0; i < data_size; ++i) + { + p->data[i].packets = packets; + p->data[i].bytes = bytes; + } + vapi_msg_vnet_interface_combined_counters_ntoh (m); + ck_assert_int_eq (_vl_msg_id, be16toh (p->_vl_msg_id)); + ck_assert_int_eq (first_sw_if_index, be32toh (p->first_sw_if_index)); + ck_assert_int_eq (be_data_size, be32toh (p->count)); + for (i = 0; i < data_size; ++i) + { + ck_assert_int_eq (packets, htobe64 (p->data[i].packets)); + ck_assert_int_eq (bytes, htobe64 (p->data[i].bytes)); + } + free (p); +} + +END_TEST; + +#define verify_ntoh_swap(expr, value) \ + if (4 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, be32toh (value)); \ + } \ + else if (2 == sizeof (expr)) \ + { \ + ck_assert_int_eq (expr, be16toh (value)); \ + } \ + else \ + { \ + ck_assert_int_eq (expr, value); \ + } + +START_TEST (test_ntoh_4) +{ + const int vla_count = 3; + char x[sizeof (vapi_msg_bridge_domain_details) + + vla_count * sizeof (vapi_type_bridge_domain_sw_if)]; + vapi_msg_bridge_domain_details *d = (void *) x; + int cnt = 1; + d->header._vl_msg_id = cnt++; + d->header.context = cnt++; + d->payload.bd_id = cnt++; + d->payload.flood = cnt++; + d->payload.uu_flood = cnt++; + d->payload.forward = cnt++; + d->payload.learn = cnt++; + d->payload.arp_term = cnt++; + d->payload.mac_age = cnt++; + d->payload.bvi_sw_if_index = cnt++; + d->payload.n_sw_ifs = htobe32 (vla_count); + int i; + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + det->context = cnt++; + det->sw_if_index = cnt++; + det->shg = cnt++; + } + vapi_msg_bridge_domain_details_ntoh (d); + ck_assert_int_eq (sizeof (x), vapi_calc_bridge_domain_details_msg_size (d)); + int tmp = 1; + verify_ntoh_swap (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + verify_ntoh_swap (d->payload.bd_id, tmp); + ++tmp; + verify_ntoh_swap (d->payload.flood, tmp); + ++tmp; + verify_ntoh_swap (d->payload.uu_flood, tmp); + ++tmp; + verify_ntoh_swap (d->payload.forward, tmp); + ++tmp; + verify_ntoh_swap (d->payload.learn, tmp); + ++tmp; + verify_ntoh_swap (d->payload.arp_term, tmp); + ++tmp; + verify_ntoh_swap (d->payload.mac_age, tmp); + ++tmp; + verify_ntoh_swap (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, vla_count); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + verify_ntoh_swap (det->context, tmp); + ++tmp; + verify_ntoh_swap (det->sw_if_index, tmp); + ++tmp; + verify_ntoh_swap (det->shg, tmp); + ++tmp; + } + vapi_msg_bridge_domain_details_hton (d); + tmp = 1; + ck_assert_int_eq (d->header._vl_msg_id, tmp); + ++tmp; + ck_assert_int_eq (d->header.context, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bd_id, tmp); + ++tmp; + ck_assert_int_eq (d->payload.flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.uu_flood, tmp); + ++tmp; + ck_assert_int_eq (d->payload.forward, tmp); + ++tmp; + ck_assert_int_eq (d->payload.learn, tmp); + ++tmp; + ck_assert_int_eq (d->payload.arp_term, tmp); + ++tmp; + ck_assert_int_eq (d->payload.mac_age, tmp); + ++tmp; + ck_assert_int_eq (d->payload.bvi_sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (d->payload.n_sw_ifs, htobe32 (vla_count)); + for (i = 0; i < vla_count; ++i) + { + vapi_type_bridge_domain_sw_if *det = &d->payload.sw_if_details[i]; + ck_assert_int_eq (det->context, tmp); + ++tmp; + ck_assert_int_eq (det->sw_if_index, tmp); + ++tmp; + ck_assert_int_eq (det->shg, tmp); + ++tmp; + } +} + +END_TEST; + +vapi_error_e +show_version_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_show_version_reply * p) +{ + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (true, is_last); + ck_assert_str_eq ("vpe", (char *) p->program); + printf + ("show_version_reply: program: `%s', version: `%s', build directory: " + "`%s', build date: `%s'\n", p->program, p->version, p->build_directory, + p->build_date); + ++*(int *) caller_ctx; + return VAPI_OK; +} + +typedef struct +{ + int called; + int expected_retval; + u32 *sw_if_index_storage; +} test_create_loopback_ctx_t; + +vapi_error_e +loopback_create_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_create_loopback_reply * p) +{ + test_create_loopback_ctx_t *clc = caller_ctx; + ck_assert_int_eq (clc->expected_retval, p->retval); + *clc->sw_if_index_storage = p->sw_if_index; + ++clc->called; + return VAPI_OK; +} + +typedef struct +{ + int called; + int expected_retval; + u32 *sw_if_index_storage; +} test_delete_loopback_ctx_t; + +vapi_error_e +loopback_delete_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_delete_loopback_reply * p) +{ + test_delete_loopback_ctx_t *dlc = caller_ctx; + ck_assert_int_eq (dlc->expected_retval, p->retval); + ++dlc->called; + return VAPI_OK; +} + +START_TEST (test_connect) +{ + vapi_ctx_t ctx; + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_disconnect (ctx); + ck_assert_int_eq (VAPI_OK, rv); + vapi_ctx_free (ctx); +} + +END_TEST; + +vapi_ctx_t ctx; + +void +setup_blocking (void) +{ + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_BLOCKING); + ck_assert_int_eq (VAPI_OK, rv); +} + +void +setup_nonblocking (void) +{ + vapi_error_e rv = vapi_ctx_alloc (&ctx); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_connect (ctx, app_name, api_prefix, max_outstanding_requests, + response_queue_size, VAPI_MODE_NONBLOCKING); + ck_assert_int_eq (VAPI_OK, rv); +} + +void +teardown (void) +{ + vapi_disconnect (ctx); + vapi_ctx_free (ctx); +} + +START_TEST (test_show_version_1) +{ + printf ("--- Basic show version message - reply test ---\n"); + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + vapi_error_e rv = vapi_send (ctx, sv); + ck_assert_int_eq (VAPI_OK, rv); + vapi_msg_show_version_reply *resp; + size_t size; + rv = vapi_recv (ctx, (void *) &resp, &size); + ck_assert_int_eq (VAPI_OK, rv); + int dummy; + show_version_cb (NULL, &dummy, VAPI_OK, true, &resp->payload); + vapi_msg_free (ctx, resp); +} + +END_TEST; + +START_TEST (test_show_version_2) +{ + int called = 0; + printf ("--- Show version via blocking callback API ---\n"); + const int attempts = response_queue_size * 4; + int i = 0; + for (i = 0; i < attempts; ++i) + { + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_error_e rv = vapi_show_version (ctx, sv, show_version_cb, &called); + ck_assert_int_eq (VAPI_OK, rv); + } + ck_assert_int_eq (attempts, called); +} + +END_TEST; + +typedef struct +{ + bool last_called; + size_t num_ifs; + u32 *sw_if_indexes; + bool *seen; + int called; +} sw_interface_dump_ctx; + +vapi_error_e +sw_interface_dump_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_sw_interface_details * reply) +{ + sw_interface_dump_ctx *dctx = callback_ctx; + ck_assert_int_eq (false, dctx->last_called); + if (is_last) + { + ck_assert (NULL == reply); + dctx->last_called = true; + } + else + { + ck_assert (reply); + printf ("Interface dump entry: [%u]: %s\n", reply->sw_if_index, + reply->interface_name); + size_t i = 0; + for (i = 0; i < dctx->num_ifs; ++i) + { + if (dctx->sw_if_indexes[i] == reply->sw_if_index) + { + ck_assert_int_eq (false, dctx->seen[i]); + dctx->seen[i] = true; + } + } + } + ++dctx->called; + return VAPI_OK; +} + +START_TEST (test_loopbacks_1) +{ + printf ("--- Create/delete loopbacks using blocking API ---\n"); + const size_t num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + u32 sw_if_indexes[num_ifs]; + memset (&sw_if_indexes, 0xff, sizeof (sw_if_indexes)); + test_create_loopback_ctx_t clcs[num_ifs]; + memset (&clcs, 0, sizeof (clcs)); + test_delete_loopback_ctx_t dlcs[num_ifs]; + memset (&dlcs, 0, sizeof (dlcs)); + int i; + for (i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + clcs[i].sw_if_index_storage = &sw_if_indexes[i]; + } + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_create_loopback *cl = vapi_alloc_create_loopback (ctx); + memcpy (cl->payload.mac_address, mac_addresses[i], + sizeof (cl->payload.mac_address)); + vapi_error_e rv = + vapi_create_loopback (ctx, cl, loopback_create_cb, &clcs[i]); + ck_assert_int_eq (VAPI_OK, rv); + } + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, clcs[i].called); + printf ("Created loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + sw_if_indexes[i]); + } + bool seen[num_ifs]; + sw_interface_dump_ctx dctx = { false, num_ifs, sw_if_indexes, seen, 0 }; + vapi_msg_sw_interface_dump *dump; + vapi_error_e rv; + const int attempts = response_queue_size * 4; + for (i = 0; i < attempts; ++i) + { + dctx.last_called = false; + memset (&seen, 0, sizeof (seen)); + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, + sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, + &dctx))) + ; + ck_assert_int_eq (true, dctx.last_called); + int j = 0; + for (j = 0; j < num_ifs; ++j) + { + ck_assert_int_eq (true, seen[j]); + } + } + memset (&seen, 0, sizeof (seen)); + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_delete_loopback *dl = vapi_alloc_delete_loopback (ctx); + dl->payload.sw_if_index = sw_if_indexes[i]; + vapi_error_e rv = + vapi_delete_loopback (ctx, dl, loopback_delete_cb, &dlcs[i]); + ck_assert_int_eq (VAPI_OK, rv); + } + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, dlcs[i].called); + printf ("Deleted loopback with sw_if_index %u\n", sw_if_indexes[i]); + } + dctx.last_called = false; + memset (&seen, 0, sizeof (seen)); + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + ck_assert_int_eq (true, dctx.last_called); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } +} + +END_TEST; + +START_TEST (test_show_version_3) +{ + printf ("--- Show version via async callback ---\n"); + int called = 0; + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (0, called); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); + called = 0; + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (0, called); +} + +END_TEST; + +START_TEST (test_show_version_4) +{ + printf ("--- Show version via async callback - multiple messages ---\n"); + vapi_error_e rv; + const size_t num_req = 5; + int contexts[num_req]; + memset (contexts, 0, sizeof (contexts)); + int i; + for (i = 0; i < num_req; ++i) + { + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = + vapi_show_version (ctx, sv, show_version_cb, &contexts[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + int j; + for (j = 0; j < num_req; ++j) + { + ck_assert_int_eq (0, contexts[j]); + } + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_req; ++i) + { + ck_assert_int_eq (1, contexts[i]); + } + memset (contexts, 0, sizeof (contexts)); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_req; ++i) + { + ck_assert_int_eq (0, contexts[i]); + } +} + +END_TEST; + +START_TEST (test_loopbacks_2) +{ + printf ("--- Create/delete loopbacks using non-blocking API ---\n"); + vapi_error_e rv; + const size_t num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + u32 sw_if_indexes[num_ifs]; + memset (&sw_if_indexes, 0xff, sizeof (sw_if_indexes)); + test_create_loopback_ctx_t clcs[num_ifs]; + memset (&clcs, 0, sizeof (clcs)); + test_delete_loopback_ctx_t dlcs[num_ifs]; + memset (&dlcs, 0, sizeof (dlcs)); + int i; + for (i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + clcs[i].sw_if_index_storage = &sw_if_indexes[i]; + } + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_create_loopback *cl = vapi_alloc_create_loopback (ctx); + memcpy (cl->payload.mac_address, mac_addresses[i], + sizeof (cl->payload.mac_address)); + while (VAPI_EAGAIN == + (rv = + vapi_create_loopback (ctx, cl, loopback_create_cb, &clcs[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, clcs[i].called); + printf ("Loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + sw_if_indexes[i]); + } + bool seen[num_ifs]; + memset (&seen, 0, sizeof (seen)); + sw_interface_dump_ctx dctx = { false, num_ifs, sw_if_indexes, seen, 0 }; + vapi_msg_sw_interface_dump *dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (false, dctx.last_called); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (true, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (true, dctx.last_called); + for (i = 0; i < num_ifs; ++i) + { + vapi_msg_delete_loopback *dl = vapi_alloc_delete_loopback (ctx); + dl->payload.sw_if_index = sw_if_indexes[i]; + while (VAPI_EAGAIN == + (rv = + vapi_delete_loopback (ctx, dl, loopback_delete_cb, &dlcs[i]))) + ; + ck_assert_int_eq (VAPI_OK, rv); + } + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, dlcs[i].called); + printf ("Deleted loopback with sw_if_index %u\n", sw_if_indexes[i]); + } + memset (&seen, 0, sizeof (seen)); + dctx.last_called = false; + dump = vapi_alloc_sw_interface_dump (ctx); + dump->payload.name_filter_valid = 0; + memset (dump->payload.name_filter, 0, sizeof (dump->payload.name_filter)); + while (VAPI_EAGAIN == + (rv = + vapi_sw_interface_dump (ctx, dump, sw_interface_dump_cb, &dctx))) + ; + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + for (i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (false, seen[i]); + } + memset (&seen, 0, sizeof (seen)); + ck_assert_int_eq (true, dctx.last_called); +} + +END_TEST; + +vapi_error_e +interface_simple_stats_cb (vapi_ctx_t ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_want_interface_simple_stats_reply * + payload) +{ + return VAPI_OK; +} + +vapi_error_e +simple_counters_cb (vapi_ctx_t ctx, void *callback_ctx, + vapi_payload_vnet_interface_simple_counters * payload) +{ + int *called = callback_ctx; + ++*called; + printf ("simple counters: first_sw_if_index=%u\n", + payload->first_sw_if_index); + return VAPI_OK; +} + +START_TEST (test_stats_1) +{ + printf ("--- Receive stats using generic blocking API ---\n"); + vapi_msg_want_interface_simple_stats *ws = + vapi_alloc_want_interface_simple_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_interface_simple_stats (ctx, ws, interface_simple_stats_cb, + NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_event_cb (ctx, vapi_msg_id_vnet_interface_simple_counters, + (vapi_event_cb) simple_counters_cb, &called); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); +} + +END_TEST; + +START_TEST (test_stats_2) +{ + printf ("--- Receive stats using stat-specific blocking API ---\n"); + vapi_msg_want_interface_simple_stats *ws = + vapi_alloc_want_interface_simple_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_interface_simple_stats (ctx, ws, interface_simple_stats_cb, + NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_vapi_msg_vnet_interface_simple_counters_event_cb (ctx, + simple_counters_cb, + &called); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); +} + +END_TEST; + +vapi_error_e +generic_cb (vapi_ctx_t ctx, void *callback_ctx, vapi_msg_id_t id, void *msg) +{ + int *called = callback_ctx; + ck_assert_int_eq (0, *called); + ++*called; + ck_assert_int_eq (id, vapi_msg_id_show_version_reply); + ck_assert_ptr_ne (NULL, msg); + vapi_msg_show_version_reply *reply = msg; + ck_assert_str_eq ("vpe", (char *) reply->payload.program); + return VAPI_OK; +} + +START_TEST (test_show_version_5) +{ + printf ("--- Receive show version using generic callback - nonblocking " + "API ---\n"); + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + while (VAPI_EAGAIN == (rv = vapi_send (ctx, sv))) + ; + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + vapi_set_generic_event_cb (ctx, generic_cb, &called); + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); + sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + vapi_msg_show_version_hton (sv); + while (VAPI_EAGAIN == (rv = vapi_send (ctx, sv))) + ; + ck_assert_int_eq (VAPI_OK, rv); + vapi_clear_generic_event_cb (ctx); + rv = vapi_dispatch_one (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, called); /* needs to remain unchanged */ +} + +END_TEST; + +vapi_error_e +combined_counters_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_payload_vnet_interface_combined_counters * payload) +{ + int *called = callback_ctx; + ++*called; + printf ("combined counters: first_sw_if_index=%u\n", + payload->first_sw_if_index); + return VAPI_OK; +} + +vapi_error_e +stats_cb (vapi_ctx_t ctx, void *callback_ctx, vapi_error_e rv, + bool is_last, vapi_payload_want_stats_reply * payload) +{ + return VAPI_OK; +} + +START_TEST (test_stats_3) +{ + printf ("--- Receive multiple stats using stat-specific non-blocking API " + "---\n"); + vapi_msg_want_stats *ws = vapi_alloc_want_stats (ctx); + ws->payload.enable_disable = 1; + ws->payload.pid = getpid (); + vapi_error_e rv; + rv = vapi_want_stats (ctx, ws, stats_cb, NULL); + ck_assert_int_eq (VAPI_OK, rv); + int called = 0; + int called2 = 0; + vapi_set_vapi_msg_vnet_interface_simple_counters_event_cb (ctx, + simple_counters_cb, + &called); + vapi_set_vapi_msg_vnet_interface_combined_counters_event_cb (ctx, + combined_counters_cb, + &called2); + while (!called || !called2) + { + if (VAPI_EAGAIN != (rv = vapi_dispatch_one (ctx))) + { + ck_assert_int_eq (VAPI_OK, rv); + } + } +} + +END_TEST; + +vapi_error_e +show_version_no_cb (vapi_ctx_t ctx, void *caller_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_show_version_reply * p) +{ + ck_assert_int_eq (VAPI_ENORESP, rv); + ck_assert_int_eq (true, is_last); + ck_assert_ptr_eq (NULL, p); + ++*(int *) caller_ctx; + return VAPI_OK; +} + +START_TEST (test_no_response_1) +{ + printf ("--- Simulate no response to regular message ---\n"); + vapi_error_e rv; + vapi_msg_show_version *sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + sv->header._vl_msg_id = ~0; /* malformed ID causes vpp to drop the msg */ + int called = 0; + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_no_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + sv = vapi_alloc_show_version (ctx); + ck_assert_ptr_ne (NULL, sv); + while (VAPI_EAGAIN == + (rv = vapi_show_version (ctx, sv, show_version_cb, &called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (2, called); +} + +END_TEST; + +vapi_error_e +no_msg_cb (struct vapi_ctx_s *ctx, void *callback_ctx, + vapi_error_e rv, bool is_last, + vapi_payload_sw_interface_details * reply) +{ + int *called = callback_ctx; + ++*called; + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (true, is_last); + ck_assert_ptr_eq (NULL, reply); + return VAPI_OK; +} + +START_TEST (test_no_response_2) +{ + printf ("--- Simulate no response to dump message ---\n"); + vapi_error_e rv; + vapi_msg_sw_interface_dump *dump = vapi_alloc_sw_interface_dump (ctx); + dump->header._vl_msg_id = ~0; /* malformed ID causes vpp to drop the msg */ + int no_called = 0; + while (VAPI_EAGAIN == + (rv = vapi_sw_interface_dump (ctx, dump, no_msg_cb, &no_called))) + ; + ck_assert_int_eq (VAPI_OK, rv); + rv = vapi_dispatch (ctx); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (1, no_called); +} + +END_TEST; + +START_TEST (test_unsupported) +{ + printf ("--- Unsupported messages ---\n"); + bool available = vapi_is_msg_available (ctx, vapi_msg_id_test_fake_msg); + ck_assert_int_eq (false, available); +} + +END_TEST; + +Suite * +test_suite (void) +{ + Suite *s = suite_create ("VAPI test"); + + TCase *tc_negative = tcase_create ("Negative tests"); + tcase_add_test (tc_negative, test_invalid_values); + suite_add_tcase (s, tc_negative); + + TCase *tc_swap = tcase_create ("Byteswap tests"); + tcase_add_test (tc_swap, test_hton_1); + tcase_add_test (tc_swap, test_hton_2); + tcase_add_test (tc_swap, test_hton_3); + tcase_add_test (tc_swap, test_hton_4); + tcase_add_test (tc_swap, test_ntoh_1); + tcase_add_test (tc_swap, test_ntoh_2); + tcase_add_test (tc_swap, test_ntoh_3); + tcase_add_test (tc_swap, test_ntoh_4); + suite_add_tcase (s, tc_swap); + + TCase *tc_connect = tcase_create ("Connect"); + tcase_add_test (tc_connect, test_connect); + suite_add_tcase (s, tc_connect); + + TCase *tc_block = tcase_create ("Blocking API"); + tcase_set_timeout (tc_block, 25); + tcase_add_checked_fixture (tc_block, setup_blocking, teardown); + tcase_add_test (tc_block, test_show_version_1); + tcase_add_test (tc_block, test_show_version_2); + tcase_add_test (tc_block, test_loopbacks_1); + tcase_add_test (tc_block, test_stats_1); + tcase_add_test (tc_block, test_stats_2); + suite_add_tcase (s, tc_block); + + TCase *tc_nonblock = tcase_create ("Nonblocking API"); + tcase_set_timeout (tc_nonblock, 25); + tcase_add_checked_fixture (tc_nonblock, setup_nonblocking, teardown); + tcase_add_test (tc_nonblock, test_show_version_3); + tcase_add_test (tc_nonblock, test_show_version_4); + tcase_add_test (tc_nonblock, test_show_version_5); + tcase_add_test (tc_nonblock, test_loopbacks_2); + tcase_add_test (tc_nonblock, test_stats_3); + tcase_add_test (tc_nonblock, test_no_response_1); + tcase_add_test (tc_nonblock, test_no_response_2); + suite_add_tcase (s, tc_nonblock); + + TCase *tc_unsupported = tcase_create ("Unsupported message"); + tcase_add_checked_fixture (tc_unsupported, setup_blocking, teardown); + tcase_add_test (tc_unsupported, test_unsupported); + suite_add_tcase (s, tc_unsupported); + + return s; +} + +int +main (int argc, char *argv[]) +{ + if (3 != argc) + { + printf ("Invalid argc==`%d'\n", argc); + return EXIT_FAILURE; + } + app_name = argv[1]; + api_prefix = argv[2]; + printf ("App name: `%s', API prefix: `%s'\n", app_name, api_prefix); + + int number_failed; + Suite *s; + SRunner *sr; + + s = test_suite (); + sr = srunner_create (s); + + srunner_run_all (sr, CK_NORMAL); + number_failed = srunner_ntests_failed (sr); + srunner_free (sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/test/ext/vapi_cpp_test.cpp b/test/ext/vapi_cpp_test.cpp new file mode 100644 index 00000000..14c35d5b --- /dev/null +++ b/test/ext/vapi_cpp_test.cpp @@ -0,0 +1,591 @@ +/* + *------------------------------------------------------------------ + * Copyright (c) 2017 Cisco and/or its affiliates. + * 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. + *------------------------------------------------------------------ + */ + +#include <memory> +#include <stdio.h> +#include <unistd.h> +#include <assert.h> +#include <setjmp.h> +#include <check.h> +#include <vapi/vapi.hpp> +#include <vapi/vpe.api.vapi.hpp> +#include <vapi/interface.api.vapi.hpp> +#include <vapi/stats.api.vapi.hpp> +#include <fake.api.vapi.hpp> + +DEFINE_VAPI_MSG_IDS_VPE_API_JSON; +DEFINE_VAPI_MSG_IDS_INTERFACE_API_JSON; +DEFINE_VAPI_MSG_IDS_STATS_API_JSON; +DEFINE_VAPI_MSG_IDS_FAKE_API_JSON; + +static char *app_name = nullptr; +static char *api_prefix = nullptr; +static const int max_outstanding_requests = 32; +static const int response_queue_size = 32; + +using namespace vapi; + +void verify_show_version_reply (const Show_version_reply &r) +{ + auto &p = r.get_payload (); + printf ("show_version_reply: program: `%s', version: `%s', build directory: " + "`%s', build date: `%s'\n", + p.program, p.version, p.build_directory, p.build_date); + ck_assert_str_eq ("vpe", (char *)p.program); +} + +Connection con; + +void setup (void) +{ + vapi_error_e rv = con.connect ( + app_name, api_prefix, max_outstanding_requests, response_queue_size); + ck_assert_int_eq (VAPI_OK, rv); +} + +void teardown (void) +{ + con.disconnect (); +} + +START_TEST (test_show_version_1) +{ + printf ("--- Show version by reading response associated to request ---\n"); + Show_version sv (con); + vapi_error_e rv = sv.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (sv); + ck_assert_int_eq (VAPI_OK, rv); + auto &r = sv.get_response (); + verify_show_version_reply (r); +} + +END_TEST; + +struct Show_version_cb +{ + Show_version_cb () : called{0} {}; + int called; + vapi_error_e operator() (Show_version &sv) + { + auto &r = sv.get_response (); + verify_show_version_reply (r); + ++called; + return VAPI_OK; + } +}; + +START_TEST (test_show_version_2) +{ + printf ("--- Show version by getting a callback ---\n"); + Show_version_cb cb; + Show_version sv (con, std::ref (cb)); + vapi_error_e rv = sv.execute (); + ck_assert_int_eq (VAPI_OK, rv); + con.dispatch (sv); + ck_assert_int_eq (1, cb.called); +} + +END_TEST; + +START_TEST (test_loopbacks_1) +{ + printf ("--- Create/delete loopbacks by waiting for response ---\n"); + const auto num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + u32 sw_if_indexes[num_ifs]; + memset (&sw_if_indexes, 0xff, sizeof (sw_if_indexes)); + for (int i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + } + for (int i = 0; i < num_ifs; ++i) + { + Create_loopback cl (con); + auto &p = cl.get_request ().get_payload (); + memcpy (p.mac_address, mac_addresses[i], sizeof (p.mac_address)); + auto e = cl.execute (); + ck_assert_int_eq (VAPI_OK, e); + vapi_error_e rv = con.wait_for_response (cl); + ck_assert_int_eq (VAPI_OK, rv); + auto &rp = cl.get_response ().get_payload (); + ck_assert_int_eq (0, rp.retval); + sw_if_indexes[i] = rp.sw_if_index; + } + for (int i = 0; i < num_ifs; ++i) + { + printf ("Created loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + sw_if_indexes[i]); + } + + { // new context + bool seen[num_ifs] = {0}; + Sw_interface_dump d (con); + auto &p = d.get_request ().get_payload (); + p.name_filter_valid = 0; + memset (p.name_filter, 0, sizeof (p.name_filter)); + auto rv = d.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (d); + ck_assert_int_eq (VAPI_OK, rv); + auto &rs = d.get_result_set (); + for (auto &r : rs) + { + auto &p = r.get_payload (); + for (int i = 0; i < num_ifs; ++i) + { + if (sw_if_indexes[i] == p.sw_if_index) + { + ck_assert_int_eq (0, seen[i]); + seen[i] = true; + } + } + } + for (int i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, seen[i]); + } + } + + for (int i = 0; i < num_ifs; ++i) + { + Delete_loopback dl (con); + dl.get_request ().get_payload ().sw_if_index = sw_if_indexes[i]; + auto rv = dl.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (dl); + ck_assert_int_eq (VAPI_OK, rv); + auto &response = dl.get_response (); + auto rp = response.get_payload (); + ck_assert_int_eq (0, rp.retval); + printf ("Deleted loopback with sw_if_index %u\n", sw_if_indexes[i]); + } + + { // new context + Sw_interface_dump d (con); + auto &p = d.get_request ().get_payload (); + p.name_filter_valid = 0; + memset (p.name_filter, 0, sizeof (p.name_filter)); + auto rv = d.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (d); + ck_assert_int_eq (VAPI_OK, rv); + auto &rs = d.get_result_set (); + for (auto &r : rs) + { + auto &p = r.get_payload (); + for (int i = 0; i < num_ifs; ++i) + { + ck_assert_int_ne (sw_if_indexes[i], p.sw_if_index); + } + } + } +} + +END_TEST; + +struct Create_loopback_cb +{ + Create_loopback_cb () : called{0}, sw_if_index{0} {}; + int called; + u32 sw_if_index; + bool seen; + vapi_error_e operator() (Create_loopback &cl) + { + auto &r = cl.get_response (); + sw_if_index = r.get_payload ().sw_if_index; + ++called; + return VAPI_OK; + } +}; + +struct Delete_loopback_cb +{ + Delete_loopback_cb () : called{0}, sw_if_index{0} {}; + int called; + u32 sw_if_index; + bool seen; + vapi_error_e operator() (Delete_loopback &dl) + { + auto &r = dl.get_response (); + ck_assert_int_eq (0, r.get_payload ().retval); + ++called; + return VAPI_OK; + } +}; + +template <int num_ifs> struct Sw_interface_dump_cb +{ + Sw_interface_dump_cb (std::array<Create_loopback_cb, num_ifs> &cbs) + : called{0}, cbs{cbs} {}; + int called; + std::array<Create_loopback_cb, num_ifs> &cbs; + vapi_error_e operator() (Sw_interface_dump &d) + { + for (auto &y : cbs) + { + y.seen = false; + } + for (auto &x : d.get_result_set ()) + { + auto &p = x.get_payload (); + for (auto &y : cbs) + { + if (p.sw_if_index == y.sw_if_index) + { + y.seen = true; + } + } + } + for (auto &y : cbs) + { + ck_assert_int_eq (true, y.seen); + } + ++called; + return VAPI_OK; + } +}; + +START_TEST (test_loopbacks_2) +{ + printf ("--- Create/delete loopbacks by getting a callback ---\n"); + const auto num_ifs = 5; + u8 mac_addresses[num_ifs][6]; + memset (&mac_addresses, 0, sizeof (mac_addresses)); + for (int i = 0; i < num_ifs; ++i) + { + memcpy (&mac_addresses[i], "\1\2\3\4\5\6", 6); + mac_addresses[i][5] = i; + } + std::array<Create_loopback_cb, num_ifs> ccbs; + std::array<std::unique_ptr<Create_loopback>, num_ifs> clcs; + for (int i = 0; i < num_ifs; ++i) + { + Create_loopback *cl = new Create_loopback (con, std::ref (ccbs[i])); + clcs[i].reset (cl); + auto &p = cl->get_request ().get_payload (); + memcpy (p.mac_address, mac_addresses[i], sizeof (p.mac_address)); + auto e = cl->execute (); + ck_assert_int_eq (VAPI_OK, e); + } + con.dispatch (); + for (int i = 0; i < num_ifs; ++i) + { + ck_assert_int_eq (1, ccbs[i].called); + printf ("Created loopback with MAC %02x:%02x:%02x:%02x:%02x:%02x --> " + "sw_if_index %u\n", + mac_addresses[i][0], mac_addresses[i][1], mac_addresses[i][2], + mac_addresses[i][3], mac_addresses[i][4], mac_addresses[i][5], + ccbs[i].sw_if_index); + } + + Sw_interface_dump_cb<num_ifs> swdcb (ccbs); + Sw_interface_dump d (con, std::ref (swdcb)); + auto &p = d.get_request ().get_payload (); + p.name_filter_valid = 0; + memset (p.name_filter, 0, sizeof (p.name_filter)); + auto rv = d.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (d); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_ne (0, swdcb.called); + std::array<Delete_loopback_cb, num_ifs> dcbs; + std::array<std::unique_ptr<Delete_loopback>, num_ifs> dlcs; + for (int i = 0; i < num_ifs; ++i) + { + Delete_loopback *dl = new Delete_loopback (con, std::ref (dcbs[i])); + dlcs[i].reset (dl); + auto &p = dl->get_request ().get_payload (); + p.sw_if_index = ccbs[i].sw_if_index; + dcbs[i].sw_if_index = ccbs[i].sw_if_index; + auto e = dl->execute (); + ck_assert_int_eq (VAPI_OK, e); + } + con.dispatch (); + for (auto &x : dcbs) + { + ck_assert_int_eq (true, x.called); + printf ("Deleted loopback with sw_if_index %u\n", x.sw_if_index); + } + + { // new context + Sw_interface_dump d (con); + auto &p = d.get_request ().get_payload (); + p.name_filter_valid = 0; + memset (p.name_filter, 0, sizeof (p.name_filter)); + auto rv = d.execute (); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (d); + ck_assert_int_eq (VAPI_OK, rv); + auto &rs = d.get_result_set (); + for (auto &r : rs) + { + auto &p = r.get_payload (); + for (int i = 0; i < num_ifs; ++i) + { + ck_assert_int_ne (ccbs[i].sw_if_index, p.sw_if_index); + } + } + } +} + +END_TEST; + +START_TEST (test_stats_1) +{ + printf ("--- Receive single stats by waiting for response ---\n"); + Want_stats ws (con); + auto &payload = ws.get_request ().get_payload (); + payload.enable_disable = 1; + payload.pid = getpid (); + auto rv = ws.execute (); + ck_assert_int_eq (VAPI_OK, rv); + Event_registration<Vnet_interface_simple_counters> sc (con); + rv = con.wait_for_response (sc); + ck_assert_int_eq (VAPI_OK, rv); + auto &rs = sc.get_result_set (); + int count = 0; + for (auto &r : rs) + { + printf ("simple counters: first_sw_if_index=%u\n", + r.get_payload ().first_sw_if_index); + ++count; + } + ck_assert_int_ne (0, count); +} + +END_TEST; + +struct Vnet_interface_simple_counters_cb +{ + Vnet_interface_simple_counters_cb () : called{0} {}; + int called; + vapi_error_e + operator() (Event_registration<Vnet_interface_simple_counters> &e) + { + ++called; + auto &rs = e.get_result_set (); + int count = 0; + for (auto &r : rs) + { + printf ("simple counters: first_sw_if_index=%u\n", + r.get_payload ().first_sw_if_index); + ++count; + } + ck_assert_int_ne (0, count); + return VAPI_OK; + } +}; + +START_TEST (test_stats_2) +{ + printf ("--- Receive single stats by getting a callback ---\n"); + Want_stats ws (con); + auto &payload = ws.get_request ().get_payload (); + payload.enable_disable = 1; + payload.pid = getpid (); + auto rv = ws.execute (); + ck_assert_int_eq (VAPI_OK, rv); + Vnet_interface_simple_counters_cb cb; + Event_registration<Vnet_interface_simple_counters> sc (con, std::ref (cb)); + rv = con.wait_for_response (sc); + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_ne (0, cb.called); +} + +END_TEST; + +struct Vnet_interface_simple_counters_2_cb +{ + Vnet_interface_simple_counters_2_cb () : called{0}, total{0} {}; + int called; + int total; + vapi_error_e + operator() (Event_registration<Vnet_interface_simple_counters> &e) + { + ++called; + auto &rs = e.get_result_set (); + int count = 0; + for (auto &r : rs) + { + printf ("simple counters: first_sw_if_index=%u\n", + r.get_payload ().first_sw_if_index); + ++count; + } + rs.free_all_responses (); + ck_assert_int_ne (0, count); + total += count; + return VAPI_OK; + } +}; + +START_TEST (test_stats_3) +{ + printf ( + "--- Receive single stats by getting a callback - clear results ---\n"); + Want_stats ws (con); + auto &payload = ws.get_request ().get_payload (); + payload.enable_disable = 1; + payload.pid = getpid (); + auto rv = ws.execute (); + ck_assert_int_eq (VAPI_OK, rv); + Vnet_interface_simple_counters_2_cb cb; + Event_registration<Vnet_interface_simple_counters> sc (con, std::ref (cb)); + for (int i = 0; i < 5; ++i) + { + rv = con.wait_for_response (sc); + } + ck_assert_int_eq (VAPI_OK, rv); + ck_assert_int_eq (5, cb.called); + ck_assert_int_eq (5, cb.total); +} + +END_TEST; + +START_TEST (test_stats_4) +{ + printf ("--- Receive multiple stats by waiting for response ---\n"); + Want_stats ws (con); + auto &payload = ws.get_request ().get_payload (); + payload.enable_disable = 1; + payload.pid = getpid (); + auto rv = ws.execute (); + ck_assert_int_eq (VAPI_OK, rv); + Event_registration<Vnet_interface_simple_counters> sc (con); + Event_registration<Vnet_interface_combined_counters> cc (con); + rv = con.wait_for_response (sc); + ck_assert_int_eq (VAPI_OK, rv); + rv = con.wait_for_response (cc); + ck_assert_int_eq (VAPI_OK, rv); + int count = 0; + for (auto &r : sc.get_result_set ()) + { + printf ("simple counters: first_sw_if_index=%u\n", + r.get_payload ().first_sw_if_index); + ++count; + } + ck_assert_int_ne (0, count); + count = 0; + for (auto &r : cc.get_result_set ()) + { + printf ("combined counters: first_sw_if_index=%u\n", + r.get_payload ().first_sw_if_index); + ++count; + } + ck_assert_int_ne (0, count); +} + +END_TEST; + +START_TEST (test_unsupported) +{ + printf ("--- Unsupported messages ---\n"); + bool thrown = false; + try + { + Test_fake_msg fake (con); + } + catch (const Msg_not_available_exception &) + { + thrown = true; + printf ("Constructing unsupported msg not possible - test pass.\n"); + } + ck_assert_int_eq (true, thrown); + thrown = false; + try + { + Test_fake_dump fake (con); + } + catch (const Msg_not_available_exception &) + { + thrown = true; + printf ("Constructing unsupported dump not possible - test pass.\n"); + } + ck_assert_int_eq (true, thrown); + thrown = false; + try + { + Event_registration<Test_fake_details> fake (con); + } + catch (const Msg_not_available_exception &) + { + thrown = true; + printf ("Constructing unsupported event registration not possible - " + "test pass.\n"); + } + ck_assert_int_eq (true, thrown); +} + +END_TEST; + +Suite *test_suite (void) +{ + Suite *s = suite_create ("VAPI test"); + + TCase *tc_cpp_api = tcase_create ("C++ API"); + tcase_set_timeout (tc_cpp_api, 25); + tcase_add_checked_fixture (tc_cpp_api, setup, teardown); + tcase_add_test (tc_cpp_api, test_show_version_1); + tcase_add_test (tc_cpp_api, test_show_version_2); + tcase_add_test (tc_cpp_api, test_loopbacks_1); + tcase_add_test (tc_cpp_api, test_loopbacks_2); + tcase_add_test (tc_cpp_api, test_stats_1); + tcase_add_test (tc_cpp_api, test_stats_2); + tcase_add_test (tc_cpp_api, test_stats_3); + tcase_add_test (tc_cpp_api, test_stats_4); + tcase_add_test (tc_cpp_api, test_unsupported); + suite_add_tcase (s, tc_cpp_api); + + return s; +} + +int main (int argc, char *argv[]) +{ + if (3 != argc) + { + printf ("Invalid argc==`%d'\n", argc); + return EXIT_FAILURE; + } + app_name = argv[1]; + api_prefix = argv[2]; + printf ("App name: `%s', API prefix: `%s'\n", app_name, api_prefix); + + int number_failed; + Suite *s; + SRunner *sr; + + s = test_suite (); + sr = srunner_create (s); + + srunner_run_all (sr, CK_NORMAL); + number_failed = srunner_ntests_failed (sr); + srunner_free (sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +/* + * fd.io coding-style-patch-verification: ON + * + * Local Variables: + * eval: (c-set-style "gnu") + * End: + */ diff --git a/test/framework.py b/test/framework.py new file mode 100644 index 00000000..600a0e54 --- /dev/null +++ b/test/framework.py @@ -0,0 +1,1017 @@ +#!/usr/bin/env python + +from __future__ import print_function +import gc +import sys +import os +import select +import unittest +import tempfile +import time +import resource +import faulthandler +from collections import deque +from threading import Thread, Event +from inspect import getdoc, isclass +from traceback import format_exception +from logging import FileHandler, DEBUG, Formatter +from scapy.packet import Raw +from hook import StepHook, PollHook +from vpp_pg_interface import VppPGInterface +from vpp_sub_interface import VppSubInterface +from vpp_lo_interface import VppLoInterface +from vpp_papi_provider import VppPapiProvider +from log import * +from vpp_object import VppObjectRegistry +if os.name == 'posix' and sys.version_info[0] < 3: + # using subprocess32 is recommended by python official documentation + # @ https://docs.python.org/2/library/subprocess.html + import subprocess32 as subprocess +else: + import subprocess + +""" + Test framework module. + + The module provides a set of tools for constructing and running tests and + representing the results. +""" + + +class _PacketInfo(object): + """Private class to create packet info object. + + Help process information about the next packet. + Set variables to default values. + """ + #: Store the index of the packet. + index = -1 + #: Store the index of the source packet generator interface of the packet. + src = -1 + #: Store the index of the destination packet generator interface + #: of the packet. + dst = -1 + #: Store expected ip version + ip = -1 + #: Store expected upper protocol + proto = -1 + #: Store the copy of the former packet. + data = None + + def __eq__(self, other): + index = self.index == other.index + src = self.src == other.src + dst = self.dst == other.dst + data = self.data == other.data + return index and src and dst and data + + +def pump_output(testclass): + """ pump output from vpp stdout/stderr to proper queues """ + while not testclass.pump_thread_stop_flag.wait(0): + readable = select.select([testclass.vpp.stdout.fileno(), + testclass.vpp.stderr.fileno(), + testclass.pump_thread_wakeup_pipe[0]], + [], [])[0] + if testclass.vpp.stdout.fileno() in readable: + read = os.read(testclass.vpp.stdout.fileno(), 1024) + testclass.vpp_stdout_deque.append(read) + if testclass.vpp.stderr.fileno() in readable: + read = os.read(testclass.vpp.stderr.fileno(), 1024) + testclass.vpp_stderr_deque.append(read) + # ignoring the dummy pipe here intentionally - the flag will take care + # of properly terminating the loop + + +def running_extended_tests(): + try: + s = os.getenv("EXTENDED_TESTS") + return True if s.lower() in ("y", "yes", "1") else False + except: + return False + return False + + +class KeepAliveReporter(object): + """ + Singleton object which reports test start to parent process + """ + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + + @property + def pipe(self): + return self._pipe + + @pipe.setter + def pipe(self, pipe): + if hasattr(self, '_pipe'): + raise Exception("Internal error - pipe should only be set once.") + self._pipe = pipe + + def send_keep_alive(self, test): + """ + Write current test tmpdir & desc to keep-alive pipe to signal liveness + """ + if self.pipe is None: + # if not running forked.. + return + + if isclass(test): + desc = test.__name__ + else: + desc = test.shortDescription() + if not desc: + desc = str(test) + + self.pipe.send((desc, test.vpp_bin, test.tempdir, test.vpp.pid)) + + +class VppTestCase(unittest.TestCase): + """This subclass is a base class for VPP test cases that are implemented as + classes. It provides methods to create and run test case. + """ + + @property + def packet_infos(self): + """List of packet infos""" + return self._packet_infos + + @classmethod + def get_packet_count_for_if_idx(cls, dst_if_index): + """Get the number of packet info for specified destination if index""" + if dst_if_index in cls._packet_count_for_dst_if_idx: + return cls._packet_count_for_dst_if_idx[dst_if_index] + else: + return 0 + + @classmethod + def instance(cls): + """Return the instance of this testcase""" + return cls.test_instance + + @classmethod + def set_debug_flags(cls, d): + cls.debug_core = False + cls.debug_gdb = False + cls.debug_gdbserver = False + if d is None: + return + dl = d.lower() + if dl == "core": + cls.debug_core = True + elif dl == "gdb": + cls.debug_gdb = True + elif dl == "gdbserver": + cls.debug_gdbserver = True + else: + raise Exception("Unrecognized DEBUG option: '%s'" % d) + + @classmethod + def setUpConstants(cls): + """ Set-up the test case class based on environment variables """ + try: + s = os.getenv("STEP") + cls.step = True if s.lower() in ("y", "yes", "1") else False + except: + cls.step = False + try: + d = os.getenv("DEBUG") + except: + d = None + cls.set_debug_flags(d) + cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") + cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') + cls.extern_plugin_path = os.getenv('EXTERN_PLUGINS') + plugin_path = None + if cls.plugin_path is not None: + if cls.extern_plugin_path is not None: + plugin_path = "%s:%s" % ( + cls.plugin_path, cls.extern_plugin_path) + else: + plugin_path = cls.plugin_path + elif cls.extern_plugin_path is not None: + plugin_path = cls.extern_plugin_path + debug_cli = "" + if cls.step or cls.debug_gdb or cls.debug_gdbserver: + debug_cli = "cli-listen localhost:5002" + coredump_size = None + try: + size = os.getenv("COREDUMP_SIZE") + if size is not None: + coredump_size = "coredump-size %s" % size + except: + pass + if coredump_size is None: + coredump_size = "coredump-size unlimited" + cls.vpp_cmdline = [cls.vpp_bin, "unix", + "{", "nodaemon", debug_cli, "full-coredump", + coredump_size, "}", "api-trace", "{", "on", "}", + "api-segment", "{", "prefix", cls.shm_prefix, "}", + "plugins", "{", "plugin", "dpdk_plugin.so", "{", + "disable", "}", "}"] + if plugin_path is not None: + cls.vpp_cmdline.extend(["plugin_path", plugin_path]) + cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline) + + @classmethod + def wait_for_enter(cls): + if cls.debug_gdbserver: + print(double_line_delim) + print("Spawned GDB server with PID: %d" % cls.vpp.pid) + elif cls.debug_gdb: + print(double_line_delim) + print("Spawned VPP with PID: %d" % cls.vpp.pid) + else: + cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid) + return + print(single_line_delim) + print("You can debug the VPP using e.g.:") + if cls.debug_gdbserver: + print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'") + print("Now is the time to attach a gdb by running the above " + "command, set up breakpoints etc. and then resume VPP from " + "within gdb by issuing the 'continue' command") + elif cls.debug_gdb: + print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid) + print("Now is the time to attach a gdb by running the above " + "command and set up breakpoints etc.") + print(single_line_delim) + raw_input("Press ENTER to continue running the testcase...") + + @classmethod + def run_vpp(cls): + cmdline = cls.vpp_cmdline + + if cls.debug_gdbserver: + gdbserver = '/usr/bin/gdbserver' + if not os.path.isfile(gdbserver) or \ + not os.access(gdbserver, os.X_OK): + raise Exception("gdbserver binary '%s' does not exist or is " + "not executable" % gdbserver) + + cmdline = [gdbserver, 'localhost:7777'] + cls.vpp_cmdline + cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline)) + + try: + cls.vpp = subprocess.Popen(cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1) + except Exception as e: + cls.logger.critical("Couldn't start vpp: %s" % e) + raise + + cls.wait_for_enter() + + @classmethod + def setUpClass(cls): + """ + Perform class setup before running the testcase + Remove shared memory files, start vpp and connect the vpp-api + """ + gc.collect() # run garbage collection first + cls.logger = getLogger(cls.__name__) + cls.tempdir = tempfile.mkdtemp( + prefix='vpp-unittest-%s-' % cls.__name__) + cls.file_handler = FileHandler("%s/log.txt" % cls.tempdir) + cls.file_handler.setFormatter( + Formatter(fmt='%(asctime)s,%(msecs)03d %(message)s', + datefmt="%H:%M:%S")) + cls.file_handler.setLevel(DEBUG) + cls.logger.addHandler(cls.file_handler) + cls.shm_prefix = cls.tempdir.split("/")[-1] + os.chdir(cls.tempdir) + cls.logger.info("Temporary dir is %s, shm prefix is %s", + cls.tempdir, cls.shm_prefix) + cls.setUpConstants() + cls.reset_packet_infos() + cls._captures = [] + cls._zombie_captures = [] + cls.verbose = 0 + cls.vpp_dead = False + cls.registry = VppObjectRegistry() + cls.vpp_startup_failed = False + cls.reporter = KeepAliveReporter() + # need to catch exceptions here because if we raise, then the cleanup + # doesn't get called and we might end with a zombie vpp + try: + cls.run_vpp() + cls.reporter.send_keep_alive(cls) + cls.vpp_stdout_deque = deque() + cls.vpp_stderr_deque = deque() + cls.pump_thread_stop_flag = Event() + cls.pump_thread_wakeup_pipe = os.pipe() + cls.pump_thread = Thread(target=pump_output, args=(cls,)) + cls.pump_thread.daemon = True + cls.pump_thread.start() + cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls) + if cls.step: + hook = StepHook(cls) + else: + hook = PollHook(cls) + cls.vapi.register_hook(hook) + cls.sleep(0.1, "after vpp startup, before initial poll") + try: + hook.poll_vpp() + except: + cls.vpp_startup_failed = True + cls.logger.critical( + "VPP died shortly after startup, check the" + " output to standard error for possible cause") + raise + try: + cls.vapi.connect() + except: + if cls.debug_gdbserver: + print(colorize("You're running VPP inside gdbserver but " + "VPP-API connection failed, did you forget " + "to 'continue' VPP from within gdb?", RED)) + raise + except: + t, v, tb = sys.exc_info() + try: + cls.quit() + except: + pass + raise t, v, tb + + @classmethod + def quit(cls): + """ + Disconnect vpp-api, kill vpp and cleanup shared memory files + """ + if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'): + cls.vpp.poll() + if cls.vpp.returncode is None: + print(double_line_delim) + print("VPP or GDB server is still running") + print(single_line_delim) + raw_input("When done debugging, press ENTER to kill the " + "process and finish running the testcase...") + + os.write(cls.pump_thread_wakeup_pipe[1], 'ding dong wake up') + cls.pump_thread_stop_flag.set() + if hasattr(cls, 'pump_thread'): + cls.logger.debug("Waiting for pump thread to stop") + cls.pump_thread.join() + if hasattr(cls, 'vpp_stderr_reader_thread'): + cls.logger.debug("Waiting for stdderr pump to stop") + cls.vpp_stderr_reader_thread.join() + + if hasattr(cls, 'vpp'): + if hasattr(cls, 'vapi'): + cls.vapi.disconnect() + del cls.vapi + cls.vpp.poll() + if cls.vpp.returncode is None: + cls.logger.debug("Sending TERM to vpp") + cls.vpp.terminate() + cls.logger.debug("Waiting for vpp to die") + cls.vpp.communicate() + del cls.vpp + + if cls.vpp_startup_failed: + stdout_log = cls.logger.info + stderr_log = cls.logger.critical + else: + stdout_log = cls.logger.info + stderr_log = cls.logger.info + + if hasattr(cls, 'vpp_stdout_deque'): + stdout_log(single_line_delim) + stdout_log('VPP output to stdout while running %s:', cls.__name__) + stdout_log(single_line_delim) + vpp_output = "".join(cls.vpp_stdout_deque) + with open(cls.tempdir + '/vpp_stdout.txt', 'w') as f: + f.write(vpp_output) + stdout_log('\n%s', vpp_output) + stdout_log(single_line_delim) + + if hasattr(cls, 'vpp_stderr_deque'): + stderr_log(single_line_delim) + stderr_log('VPP output to stderr while running %s:', cls.__name__) + stderr_log(single_line_delim) + vpp_output = "".join(cls.vpp_stderr_deque) + with open(cls.tempdir + '/vpp_stderr.txt', 'w') as f: + f.write(vpp_output) + stderr_log('\n%s', vpp_output) + stderr_log(single_line_delim) + + @classmethod + def tearDownClass(cls): + """ Perform final cleanup after running all tests in this test-case """ + cls.quit() + cls.file_handler.close() + + def tearDown(self): + """ Show various debug prints after each test """ + self.logger.debug("--- tearDown() for %s.%s(%s) called ---" % + (self.__class__.__name__, self._testMethodName, + self._testMethodDoc)) + if not self.vpp_dead: + self.logger.debug(self.vapi.cli("show trace")) + self.logger.info(self.vapi.ppcli("show interface")) + self.logger.info(self.vapi.ppcli("show hardware")) + self.logger.info(self.vapi.ppcli("show error")) + self.logger.info(self.vapi.ppcli("show run")) + self.registry.remove_vpp_config(self.logger) + # Save/Dump VPP api trace log + api_trace = "vpp_api_trace.%s.log" % self._testMethodName + tmp_api_trace = "/tmp/%s" % api_trace + vpp_api_trace_log = "%s/%s" % (self.tempdir, api_trace) + self.logger.info(self.vapi.ppcli("api trace save %s" % api_trace)) + self.logger.info("Moving %s to %s\n" % (tmp_api_trace, + vpp_api_trace_log)) + os.rename(tmp_api_trace, vpp_api_trace_log) + self.logger.info(self.vapi.ppcli("api trace dump %s" % + vpp_api_trace_log)) + else: + self.registry.unregister_all(self.logger) + + def setUp(self): + """ Clear trace before running each test""" + self.reporter.send_keep_alive(self) + self.logger.debug("--- setUp() for %s.%s(%s) called ---" % + (self.__class__.__name__, self._testMethodName, + self._testMethodDoc)) + if self.vpp_dead: + raise Exception("VPP is dead when setting up the test") + self.sleep(.1, "during setUp") + self.vpp_stdout_deque.append( + "--- test setUp() for %s.%s(%s) starts here ---\n" % + (self.__class__.__name__, self._testMethodName, + self._testMethodDoc)) + self.vpp_stderr_deque.append( + "--- test setUp() for %s.%s(%s) starts here ---\n" % + (self.__class__.__name__, self._testMethodName, + self._testMethodDoc)) + self.vapi.cli("clear trace") + # store the test instance inside the test class - so that objects + # holding the class can access instance methods (like assertEqual) + type(self).test_instance = self + + @classmethod + def pg_enable_capture(cls, interfaces): + """ + Enable capture on packet-generator interfaces + + :param interfaces: iterable interface indexes + + """ + for i in interfaces: + i.enable_capture() + + @classmethod + def register_capture(cls, cap_name): + """ Register a capture in the testclass """ + # add to the list of captures with current timestamp + cls._captures.append((time.time(), cap_name)) + # filter out from zombies + cls._zombie_captures = [(stamp, name) + for (stamp, name) in cls._zombie_captures + if name != cap_name] + + @classmethod + def pg_start(cls): + """ Remove any zombie captures and enable the packet generator """ + # how long before capture is allowed to be deleted - otherwise vpp + # crashes - 100ms seems enough (this shouldn't be needed at all) + capture_ttl = 0.1 + now = time.time() + for stamp, cap_name in cls._zombie_captures: + wait = stamp + capture_ttl - now + if wait > 0: + cls.sleep(wait, "before deleting capture %s" % cap_name) + now = time.time() + cls.logger.debug("Removing zombie capture %s" % cap_name) + cls.vapi.cli('packet-generator delete %s' % cap_name) + + cls.vapi.cli("trace add pg-input 50") # 50 is maximum + cls.vapi.cli('packet-generator enable') + cls._zombie_captures = cls._captures + cls._captures = [] + + @classmethod + def create_pg_interfaces(cls, interfaces): + """ + Create packet-generator interfaces. + + :param interfaces: iterable indexes of the interfaces. + :returns: List of created interfaces. + + """ + result = [] + for i in interfaces: + intf = VppPGInterface(cls, i) + setattr(cls, intf.name, intf) + result.append(intf) + cls.pg_interfaces = result + return result + + @classmethod + def create_loopback_interfaces(cls, interfaces): + """ + Create loopback interfaces. + + :param interfaces: iterable indexes of the interfaces. + :returns: List of created interfaces. + """ + result = [] + for i in interfaces: + intf = VppLoInterface(cls, i) + setattr(cls, intf.name, intf) + result.append(intf) + cls.lo_interfaces = result + return result + + @staticmethod + def extend_packet(packet, size): + """ + Extend packet to given size by padding with spaces + NOTE: Currently works only when Raw layer is present. + + :param packet: packet + :param size: target size + + """ + packet_len = len(packet) + 4 + extend = size - packet_len + if extend > 0: + packet[Raw].load += ' ' * extend + + @classmethod + def reset_packet_infos(cls): + """ Reset the list of packet info objects and packet counts to zero """ + cls._packet_infos = {} + cls._packet_count_for_dst_if_idx = {} + + @classmethod + def create_packet_info(cls, src_if, dst_if): + """ + Create packet info object containing the source and destination indexes + and add it to the testcase's packet info list + + :param VppInterface src_if: source interface + :param VppInterface dst_if: destination interface + + :returns: _PacketInfo object + + """ + info = _PacketInfo() + info.index = len(cls._packet_infos) + info.src = src_if.sw_if_index + info.dst = dst_if.sw_if_index + if isinstance(dst_if, VppSubInterface): + dst_idx = dst_if.parent.sw_if_index + else: + dst_idx = dst_if.sw_if_index + if dst_idx in cls._packet_count_for_dst_if_idx: + cls._packet_count_for_dst_if_idx[dst_idx] += 1 + else: + cls._packet_count_for_dst_if_idx[dst_idx] = 1 + cls._packet_infos[info.index] = info + return info + + @staticmethod + def info_to_payload(info): + """ + Convert _PacketInfo object to packet payload + + :param info: _PacketInfo object + + :returns: string containing serialized data from packet info + """ + return "%d %d %d %d %d" % (info.index, info.src, info.dst, + info.ip, info.proto) + + @staticmethod + def payload_to_info(payload): + """ + Convert packet payload to _PacketInfo object + + :param payload: packet payload + + :returns: _PacketInfo object containing de-serialized data from payload + + """ + numbers = payload.split() + info = _PacketInfo() + info.index = int(numbers[0]) + info.src = int(numbers[1]) + info.dst = int(numbers[2]) + info.ip = int(numbers[3]) + info.proto = int(numbers[4]) + return info + + def get_next_packet_info(self, info): + """ + Iterate over the packet info list stored in the testcase + Start iteration with first element if info is None + Continue based on index in info if info is specified + + :param info: info or None + :returns: next info in list or None if no more infos + """ + if info is None: + next_index = 0 + else: + next_index = info.index + 1 + if next_index == len(self._packet_infos): + return None + else: + return self._packet_infos[next_index] + + def get_next_packet_info_for_interface(self, src_index, info): + """ + Search the packet info list for the next packet info with same source + interface index + + :param src_index: source interface index to search for + :param info: packet info - where to start the search + :returns: packet info or None + + """ + while True: + info = self.get_next_packet_info(info) + if info is None: + return None + if info.src == src_index: + return info + + def get_next_packet_info_for_interface2(self, src_index, dst_index, info): + """ + Search the packet info list for the next packet info with same source + and destination interface indexes + + :param src_index: source interface index to search for + :param dst_index: destination interface index to search for + :param info: packet info - where to start the search + :returns: packet info or None + + """ + while True: + info = self.get_next_packet_info_for_interface(src_index, info) + if info is None: + return None + if info.dst == dst_index: + return info + + def assert_equal(self, real_value, expected_value, name_or_class=None): + if name_or_class is None: + self.assertEqual(real_value, expected_value) + return + try: + msg = "Invalid %s: %d('%s') does not match expected value %d('%s')" + msg = msg % (getdoc(name_or_class).strip(), + real_value, str(name_or_class(real_value)), + expected_value, str(name_or_class(expected_value))) + except: + msg = "Invalid %s: %s does not match expected value %s" % ( + name_or_class, real_value, expected_value) + + self.assertEqual(real_value, expected_value, msg) + + def assert_in_range(self, + real_value, + expected_min, + expected_max, + name=None): + if name is None: + msg = None + else: + msg = "Invalid %s: %s out of range <%s,%s>" % ( + name, real_value, expected_min, expected_max) + self.assertTrue(expected_min <= real_value <= expected_max, msg) + + @classmethod + def sleep(cls, timeout, remark=None): + if hasattr(cls, 'logger'): + cls.logger.debug("Starting sleep for %ss (%s)" % (timeout, remark)) + before = time.time() + time.sleep(timeout) + after = time.time() + if after - before > 2 * timeout: + cls.logger.error("unexpected time.sleep() result - " + "slept for %ss instead of ~%ss!" % ( + after - before, timeout)) + if hasattr(cls, 'logger'): + cls.logger.debug( + "Finished sleep (%s) - slept %ss (wanted %ss)" % ( + remark, after - before, timeout)) + + +class TestCasePrinter(object): + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + if not hasattr(self, "_test_case_set"): + self._test_case_set = set() + + def print_test_case_heading_if_first_time(self, case): + if case.__class__ not in self._test_case_set: + print(double_line_delim) + print(colorize(getdoc(case.__class__).splitlines()[0], YELLOW)) + print(double_line_delim) + self._test_case_set.add(case.__class__) + + +class VppTestResult(unittest.TestResult): + """ + @property result_string + String variable to store the test case result string. + @property errors + List variable containing 2-tuples of TestCase instances and strings + holding formatted tracebacks. Each tuple represents a test which + raised an unexpected exception. + @property failures + List variable containing 2-tuples of TestCase instances and strings + holding formatted tracebacks. Each tuple represents a test where + a failure was explicitly signalled using the TestCase.assert*() + methods. + """ + + def __init__(self, stream, descriptions, verbosity): + """ + :param stream File descriptor to store where to report test results. + Set to the standard error stream by default. + :param descriptions Boolean variable to store information if to use + test case descriptions. + :param verbosity Integer variable to store required verbosity level. + """ + unittest.TestResult.__init__(self, stream, descriptions, verbosity) + self.stream = stream + self.descriptions = descriptions + self.verbosity = verbosity + self.result_string = None + self.printer = TestCasePrinter() + + def addSuccess(self, test): + """ + Record a test succeeded result + + :param test: + + """ + if hasattr(test, 'logger'): + test.logger.debug("--- addSuccess() %s.%s(%s) called" + % (test.__class__.__name__, + test._testMethodName, + test._testMethodDoc)) + unittest.TestResult.addSuccess(self, test) + self.result_string = colorize("OK", GREEN) + + def addSkip(self, test, reason): + """ + Record a test skipped. + + :param test: + :param reason: + + """ + if hasattr(test, 'logger'): + test.logger.debug("--- addSkip() %s.%s(%s) called, reason is %s" + % (test.__class__.__name__, + test._testMethodName, + test._testMethodDoc, + reason)) + unittest.TestResult.addSkip(self, test, reason) + self.result_string = colorize("SKIP", YELLOW) + + def symlink_failed(self, test): + logger = None + if hasattr(test, 'logger'): + logger = test.logger + if hasattr(test, 'tempdir'): + try: + failed_dir = os.getenv('VPP_TEST_FAILED_DIR') + link_path = '%s/%s-FAILED' % (failed_dir, + test.tempdir.split("/")[-1]) + if logger: + logger.debug("creating a link to the failed test") + logger.debug("os.symlink(%s, %s)" % + (test.tempdir, link_path)) + os.symlink(test.tempdir, link_path) + except Exception as e: + if logger: + logger.error(e) + + def addFailure(self, test, err): + """ + Record a test failed result + + :param test: + :param err: error message + + """ + if hasattr(test, 'logger'): + test.logger.debug("--- addFailure() %s.%s(%s) called, err is %s" + % (test.__class__.__name__, + test._testMethodName, + test._testMethodDoc, err)) + test.logger.debug("formatted exception is:\n%s" % + "".join(format_exception(*err))) + unittest.TestResult.addFailure(self, test, err) + if hasattr(test, 'tempdir'): + self.result_string = colorize("FAIL", RED) + \ + ' [ temp dir used by test case: ' + test.tempdir + ' ]' + self.symlink_failed(test) + else: + self.result_string = colorize("FAIL", RED) + ' [no temp dir]' + + def addError(self, test, err): + """ + Record a test error result + + :param test: + :param err: error message + + """ + if hasattr(test, 'logger'): + test.logger.debug("--- addError() %s.%s(%s) called, err is %s" + % (test.__class__.__name__, + test._testMethodName, + test._testMethodDoc, err)) + test.logger.debug("formatted exception is:\n%s" % + "".join(format_exception(*err))) + unittest.TestResult.addError(self, test, err) + if hasattr(test, 'tempdir'): + self.result_string = colorize("ERROR", RED) + \ + ' [ temp dir used by test case: ' + test.tempdir + ' ]' + self.symlink_failed(test) + else: + self.result_string = colorize("ERROR", RED) + ' [no temp dir]' + + def getDescription(self, test): + """ + Get test description + + :param test: + :returns: test description + + """ + # TODO: if none print warning not raise exception + short_description = test.shortDescription() + if self.descriptions and short_description: + return short_description + else: + return str(test) + + def startTest(self, test): + """ + Start a test + + :param test: + + """ + self.printer.print_test_case_heading_if_first_time(test) + unittest.TestResult.startTest(self, test) + if self.verbosity > 0: + self.stream.writeln( + "Starting " + self.getDescription(test) + " ...") + self.stream.writeln(single_line_delim) + + def stopTest(self, test): + """ + Stop a test + + :param test: + + """ + unittest.TestResult.stopTest(self, test) + if self.verbosity > 0: + self.stream.writeln(single_line_delim) + self.stream.writeln("%-73s%s" % (self.getDescription(test), + self.result_string)) + self.stream.writeln(single_line_delim) + else: + self.stream.writeln("%-73s%s" % (self.getDescription(test), + self.result_string)) + + def printErrors(self): + """ + Print errors from running the test case + """ + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + """ + Print error list to the output stream together with error type + and test case description. + + :param flavour: error type + :param errors: iterable errors + + """ + for test, err in errors: + self.stream.writeln(double_line_delim) + self.stream.writeln("%s: %s" % + (flavour, self.getDescription(test))) + self.stream.writeln(single_line_delim) + self.stream.writeln("%s" % err) + + +class VppTestRunner(unittest.TextTestRunner): + """ + A basic test runner implementation which prints results to standard error. + """ + @property + def resultclass(self): + """Class maintaining the results of the tests""" + return VppTestResult + + def __init__(self, pipe=None, stream=sys.stderr, descriptions=True, + verbosity=1, failfast=False, buffer=False, resultclass=None): + # ignore stream setting here, use hard-coded stdout to be in sync + # with prints from VppTestCase methods ... + super(VppTestRunner, self).__init__(sys.stdout, descriptions, + verbosity, failfast, buffer, + resultclass) + reporter = KeepAliveReporter() + reporter.pipe = pipe + + test_option = "TEST" + + def parse_test_option(self): + try: + f = os.getenv(self.test_option) + except: + f = None + filter_file_name = None + filter_class_name = None + filter_func_name = None + if f: + if '.' in f: + parts = f.split('.') + if len(parts) > 3: + raise Exception("Unrecognized %s option: %s" % + (self.test_option, f)) + if len(parts) > 2: + if parts[2] not in ('*', ''): + filter_func_name = parts[2] + if parts[1] not in ('*', ''): + filter_class_name = parts[1] + if parts[0] not in ('*', ''): + if parts[0].startswith('test_'): + filter_file_name = parts[0] + else: + filter_file_name = 'test_%s' % parts[0] + else: + if f.startswith('test_'): + filter_file_name = f + else: + filter_file_name = 'test_%s' % f + return filter_file_name, filter_class_name, filter_func_name + + def filter_tests(self, tests, filter_file, filter_class, filter_func): + result = unittest.suite.TestSuite() + for t in tests: + if isinstance(t, unittest.suite.TestSuite): + # this is a bunch of tests, recursively filter... + x = self.filter_tests(t, filter_file, filter_class, + filter_func) + if x.countTestCases() > 0: + result.addTest(x) + elif isinstance(t, unittest.TestCase): + # this is a single test + parts = t.id().split('.') + # t.id() for common cases like this: + # test_classifier.TestClassifier.test_acl_ip + # apply filtering only if it is so + if len(parts) == 3: + if filter_file and filter_file != parts[0]: + continue + if filter_class and filter_class != parts[1]: + continue + if filter_func and filter_func != parts[2]: + continue + result.addTest(t) + else: + # unexpected object, don't touch it + result.addTest(t) + return result + + def run(self, test): + """ + Run the tests + + :param test: + + """ + faulthandler.enable() # emit stack trace to stderr if killed by signal + print("Running tests using custom test runner") # debug message + filter_file, filter_class, filter_func = self.parse_test_option() + print("Active filters: file=%s, class=%s, function=%s" % ( + filter_file, filter_class, filter_func)) + filtered = self.filter_tests(test, filter_file, filter_class, + filter_func) + print("%s out of %s tests match specified filters" % ( + filtered.countTestCases(), test.countTestCases())) + if not running_extended_tests(): + print("Not running extended tests (some tests will be skipped)") + return super(VppTestRunner, self).run(filtered) diff --git a/test/hook.py b/test/hook.py new file mode 100644 index 00000000..f34e0c5b --- /dev/null +++ b/test/hook.py @@ -0,0 +1,212 @@ +import signal +import os +import traceback +from log import RED, single_line_delim, double_line_delim +from debug import spawn_gdb, gdb_path + + +class Hook(object): + """ + Generic hooks before/after API/CLI calls + """ + + def __init__(self, logger): + self.logger = logger + + def before_api(self, api_name, api_args): + """ + Function called before API call + Emit a debug message describing the API name and arguments + + @param api_name: name of the API + @param api_args: tuple containing the API arguments + """ + self.logger.debug("API: %s (%s)" % + (api_name, api_args), extra={'color': RED}) + + def after_api(self, api_name, api_args): + """ + Function called after API call + + @param api_name: name of the API + @param api_args: tuple containing the API arguments + """ + pass + + def before_cli(self, cli): + """ + Function called before CLI call + Emit a debug message describing the CLI + + @param cli: CLI string + """ + self.logger.debug("CLI: %s" % (cli), extra={'color': RED}) + + def after_cli(self, cli): + """ + Function called after CLI call + """ + pass + + +class VppDiedError(Exception): + pass + + +class PollHook(Hook): + """ Hook which checks if the vpp subprocess is alive """ + + def __init__(self, testcase): + self.testcase = testcase + self.logger = testcase.logger + + def on_crash(self, core_path): + if self.testcase.debug_core: + if not spawn_gdb(self.testcase.vpp_bin, core_path): + self.logger.error( + "Debugger '%s' does not exist or is not an executable.." % + gdb_path) + else: + return + self.logger.critical("Core file present, debug with: gdb %s %s" % + (self.testcase.vpp_bin, core_path)) + + def poll_vpp(self): + """ + Poll the vpp status and throw an exception if it's not running + :raises VppDiedError: exception if VPP is not running anymore + """ + if self.testcase.vpp_dead: + # already dead, nothing to do + return + + self.testcase.vpp.poll() + if self.testcase.vpp.returncode is not None: + signaldict = dict( + (k, v) for v, k in reversed(sorted(signal.__dict__.items())) + if v.startswith('SIG') and not v.startswith('SIG_')) + + if self.testcase.vpp.returncode in signaldict: + s = signaldict[abs(self.testcase.vpp.returncode)] + else: + s = "unknown" + msg = "VPP subprocess died unexpectedly with returncode %d [%s]" %\ + (self.testcase.vpp.returncode, s) + self.logger.critical(msg) + core_path = self.testcase.tempdir + '/core' + if os.path.isfile(core_path): + self.on_crash(core_path) + self.testcase.vpp_dead = True + raise VppDiedError(msg) + + def before_api(self, api_name, api_args): + """ + Check if VPP died before executing an API + + :param api_name: name of the API + :param api_args: tuple containing the API arguments + :raises VppDiedError: exception if VPP is not running anymore + + """ + super(PollHook, self).before_api(api_name, api_args) + self.poll_vpp() + + def before_cli(self, cli): + """ + Check if VPP died before executing a CLI + + :param cli: CLI string + :raises Exception: exception if VPP is not running anymore + + """ + super(PollHook, self).before_cli(cli) + self.poll_vpp() + + +class StepHook(PollHook): + """ Hook which requires user to press ENTER before doing any API/CLI """ + + def __init__(self, testcase): + self.skip_stack = None + self.skip_num = None + self.skip_count = 0 + super(StepHook, self).__init__(testcase) + + def skip(self): + if self.skip_stack is None: + return False + stack = traceback.extract_stack() + counter = 0 + skip = True + for e in stack: + if counter > self.skip_num: + break + if e[0] != self.skip_stack[counter][0]: + skip = False + if e[1] != self.skip_stack[counter][1]: + skip = False + counter += 1 + if skip: + self.skip_count += 1 + return True + else: + print("%d API/CLI calls skipped in specified stack " + "frame" % self.skip_count) + self.skip_count = 0 + self.skip_stack = None + self.skip_num = None + return False + + def user_input(self): + print('number\tfunction\tfile\tcode') + counter = 0 + stack = traceback.extract_stack() + for e in stack: + print('%02d.\t%s\t%s:%d\t[%s]' % (counter, e[2], e[0], e[1], e[3])) + counter += 1 + print(single_line_delim) + print("You can enter a number of stack frame chosen from above") + print("Calls in/below that stack frame will be not be stepped anymore") + print(single_line_delim) + while True: + choice = raw_input("Enter your choice, if any, and press ENTER to " + "continue running the testcase...") + if choice == "": + choice = None + try: + if choice is not None: + num = int(choice) + except: + print("Invalid input") + continue + if choice is not None and (num < 0 or num >= len(stack)): + print("Invalid choice") + continue + break + if choice is not None: + self.skip_stack = stack + self.skip_num = num + + def before_cli(self, cli): + """ Wait for ENTER before executing CLI """ + if self.skip(): + print("Skip pause before executing CLI: %s" % cli) + else: + print(double_line_delim) + print("Test paused before executing CLI: %s" % cli) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_cli(cli) + + def before_api(self, api_name, api_args): + """ Wait for ENTER before executing API """ + if self.skip(): + print("Skip pause before executing API: %s (%s)" + % (api_name, api_args)) + else: + print(double_line_delim) + print("Test paused before executing API: %s (%s)" + % (api_name, api_args)) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_api(api_name, api_args) diff --git a/test/ipfix.py b/test/ipfix.py new file mode 100644 index 00000000..deaff67b --- /dev/null +++ b/test/ipfix.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python +# IPFIX support for Scapy (RFC7011) + +from scapy.all import * + + +# IPFIX Information Elements http://www.iana.org/assignments/ipfix/ipfix.xhtml +information_elements = { + 1: "octetDeltaCount", + 2: "packetDeltaCount", + 3: "deltaFlowCount", + 4: "protocolIdentifier", + 5: "ipClassOfService", + 6: "tcpControlBits", + 7: "sourceTransportPort", + 8: "sourceIPv4Address", + 9: "sourceIPv4PrefixLength", + 10: "ingressInterface", + 11: "destinationTransportPort", + 12: "destinationIPv4Address", + 13: "destinationIPv4PrefixLength", + 14: "egressInterface", + 15: "ipNextHopIPv4Address", + 16: "bgpSourceAsNumber", + 17: "bgpDestinationAsNumber", + 18: "bgpNextHopIPv4Address", + 19: "postMCastPacketDeltaCount", + 20: "postMCastOctetDeltaCount", + 21: "flowEndSysUpTime", + 22: "flowStartSysUpTime", + 23: "postOctetDeltaCount", + 24: "postPacketDeltaCount", + 25: "minimumIpTotalLength", + 26: "maximumIpTotalLength", + 27: "sourceIPv6Address", + 28: "destinationIPv6Address", + 29: "sourceIPv6PrefixLength", + 30: "destinationIPv6PrefixLength", + 31: "flowLabelIPv6", + 32: "icmpTypeCodeIPv4", + 33: "igmpType", + 34: "samplingInterval", + 35: "samplingAlgorithm", + 36: "flowActiveTimeout", + 37: "flowIdleTimeout", + 38: "engineType", + 39: "engineId", + 40: "exportedOctetTotalCount", + 41: "exportedMessageTotalCount", + 42: "exportedFlowRecordTotalCount", + 43: "ipv4RouterSc", + 44: "sourceIPv4Prefix", + 45: "destinationIPv4Prefix", + 46: "mplsTopLabelType", + 47: "mplsTopLabelIPv4Address", + 48: "samplerId", + 49: "samplerMode", + 50: "samplerRandomInterval", + 51: "classId", + 52: "minimumTTL", + 53: "maximumTTL", + 54: "fragmentIdentification", + 55: "postIpClassOfService", + 56: "sourceMacAddress", + 57: "postDestinationMacAddress", + 58: "vlanId", + 59: "postVlanId", + 60: "ipVersion", + 61: "flowDirection", + 62: "ipNextHopIPv6Address", + 63: "bgpNextHopIPv6Address", + 64: "ipv6ExtensionHeaders", + 70: "mplsTopLabelStackSection", + 71: "mplsLabelStackSection2", + 72: "mplsLabelStackSection3", + 73: "mplsLabelStackSection4", + 74: "mplsLabelStackSection5", + 75: "mplsLabelStackSection6", + 76: "mplsLabelStackSection7", + 77: "mplsLabelStackSection8", + 78: "mplsLabelStackSection9", + 79: "mplsLabelStackSection10", + 80: "destinationMacAddress", + 81: "postSourceMacAddress", + 82: "interfaceName", + 83: "interfaceDescription", + 84: "samplerName", + 85: "octetTotalCount", + 86: "packetTotalCount", + 87: "flagsAndSamplerId", + 88: "fragmentOffset", + 89: "forwardingStatus", + 90: "mplsVpnRouteDistinguisher", + 91: "mplsTopLabelPrefixLength", + 92: "srcTrafficIndex", + 93: "dstTrafficIndex", + 94: "applicationDescription", + 95: "applicationId", + 96: "applicationName", + 98: "postIpDiffServCodePoint", + 99: "multicastReplicationFactor", + 100: "className", + 101: "classificationEngineId", + 102: "layer2packetSectionOffset", + 103: "layer2packetSectionSize", + 104: "layer2packetSectionData", + 128: "bgpNextAdjacentAsNumber", + 129: "bgpPrevAdjacentAsNumber", + 130: "exporterIPv4Address", + 131: "exporterIPv6Address", + 132: "droppedOctetDeltaCount", + 133: "droppedPacketDeltaCount", + 134: "droppedOctetTotalCount", + 135: "droppedPacketTotalCount", + 136: "flowEndReason", + 137: "commonPropertiesId", + 138: "observationPointId", + 139: "icmpTypeCodeIPv6", + 140: "mplsTopLabelIPv6Address", + 141: "lineCardId", + 142: "portId", + 143: "meteringProcessId", + 144: "exportingProcessId", + 145: "templateId", + 146: "wlanChannelId", + 147: "wlanSSID", + 148: "flowId", + 149: "observationDomainId", + 150: "flowStartSeconds", + 151: "flowEndSeconds", + 152: "flowStartMilliseconds", + 153: "flowEndMilliseconds", + 154: "flowStartMicroseconds", + 155: "flowEndMicroseconds", + 156: "flowStartNanoseconds", + 157: "flowEndNanoseconds", + 158: "flowStartDeltaMicroseconds", + 159: "flowEndDeltaMicroseconds", + 160: "systemInitTimeMilliseconds", + 161: "flowDurationMilliseconds", + 162: "flowDurationMicroseconds", + 163: "observedFlowTotalCount", + 164: "ignoredPacketTotalCount", + 165: "ignoredOctetTotalCount", + 166: "notSentFlowTotalCount", + 167: "notSentPacketTotalCount", + 168: "notSentOctetTotalCount", + 169: "destinationIPv6Prefix", + 170: "sourceIPv6Prefix", + 171: "postOctetTotalCount", + 172: "postPacketTotalCount", + 173: "flowKeyIndicator", + 174: "postMCastPacketTotalCount", + 175: "postMCastOctetTotalCount", + 176: "icmpTypeIPv4", + 177: "icmpCodeIPv4", + 178: "icmpTypeIPv6", + 179: "icmpCodeIPv6", + 180: "udpSourcePort", + 181: "udpDestinationPort", + 182: "tcpSourcePort", + 183: "tcpDestinationPort", + 184: "tcpSequenceNumber", + 185: "tcpAcknowledgementNumber", + 186: "tcpWindowSize", + 187: "tcpUrgentPointer", + 188: "tcpHeaderLength", + 189: "ipHeaderLength", + 190: "totalLengthIPv4", + 191: "payloadLengthIPv6", + 192: "ipTTL", + 193: "nextHeaderIPv6", + 194: "mplsPayloadLength", + 195: "ipDiffServCodePoint", + 196: "ipPrecedence", + 197: "fragmentFlags", + 198: "octetDeltaSumOfSquares", + 199: "octetTotalSumOfSquares", + 200: "mplsTopLabelTTL", + 201: "mplsLabelStackLength", + 202: "mplsLabelStackDepth", + 203: "mplsTopLabelExp", + 204: "ipPayloadLength", + 205: "udpMessageLength", + 206: "isMulticast", + 207: "ipv4IHL", + 208: "ipv4Options", + 209: "tcpOptions", + 210: "paddingOctets", + 211: "collectorIPv4Address", + 212: "collectorIPv6Address", + 213: "exportInterface", + 214: "exportProtocolVersion", + 215: "exportTransportProtocol", + 216: "collectorTransportPort", + 217: "exporterTransportPort", + 218: "tcpSynTotalCount", + 219: "tcpFinTotalCount", + 220: "tcpRstTotalCount", + 221: "tcpPshTotalCount", + 222: "tcpAckTotalCount", + 223: "tcpUrgTotalCount", + 224: "ipTotalLength", + 225: "postNATSourceIPv4Address", + 226: "postNATDestinationIPv4Address", + 227: "postNAPTSourceTransportPort", + 228: "postNAPTDestinationTransportPort", + 229: "natOriginatingAddressRealm", + 230: "natEvent", + 231: "initiatorOctets", + 232: "responderOctets", + 233: "firewallEvent", + 234: "ingressVRFID", + 235: "egressVRFID", + 236: "VRFname", + 237: "postMplsTopLabelExp", + 238: "tcpWindowScale", + 239: "biflowDirection", + 240: "ethernetHeaderLength", + 241: "ethernetPayloadLength", + 242: "ethernetTotalLength", + 243: "dot1qVlanId", + 244: "dot1qPriority", + 245: "dot1qCustomerVlanId", + 246: "dot1qCustomerPriority", + 247: "metroEvcId", + 248: "metroEvcType", + 249: "pseudoWireId", + 250: "pseudoWireType", + 251: "pseudoWireControlWord", + 252: "ingressPhysicalInterface", + 253: "egressPhysicalInterface", + 254: "postDot1qVlanId", + 255: "postDot1qCustomerVlanId", + 256: "ethernetType", + 257: "postIpPrecedence", + 258: "collectionTimeMilliseconds", + 259: "exportSctpStreamId", + 260: "maxExportSeconds", + 261: "maxFlowEndSeconds", + 262: "messageMD5Checksum", + 263: "messageScope", + 264: "minExportSeconds", + 265: "minFlowStartSeconds", + 266: "opaqueOctets", + 267: "sessionScope", + 268: "maxFlowEndMicroseconds", + 269: "maxFlowEndMilliseconds", + 270: "maxFlowEndNanoseconds", + 271: "minFlowStartMicroseconds", + 272: "minFlowStartMilliseconds", + 273: "minFlowStartNanoseconds", + 274: "collectorCertificate", + 275: "exporterCertificate", + 276: "dataRecordsReliability", + 277: "observationPointType", + 278: "newConnectionDeltaCount", + 279: "connectionSumDurationSeconds", + 280: "connectionTransactionId", + 281: "postNATSourceIPv6Address", + 282: "postNATDestinationIPv6Address", + 283: "natPoolId", + 284: "natPoolName", + 285: "anonymizationFlags", + 286: "anonymizationTechnique", + 287: "informationElementIndex", + 288: "p2pTechnology", + 289: "tunnelTechnology", + 290: "encryptedTechnology", + 291: "basicList", + 292: "subTemplateList", + 293: "subTemplateMultiList", + 294: "bgpValidityState", + 295: "IPSecSPI", + 296: "greKey", + 297: "natType", + 298: "initiatorPackets", + 299: "responderPackets", + 300: "observationDomainName", + 301: "selectionSequenceId", + 302: "selectorId", + 303: "informationElementId", + 304: "selectorAlgorithm", + 305: "samplingPacketInterval", + 306: "samplingPacketSpace", + 307: "samplingTimeInterval", + 308: "samplingTimeSpace", + 309: "samplingSize", + 310: "samplingPopulation", + 311: "samplingProbability", + 312: "dataLinkFrameSize", + 313: "ipHeaderPacketSection", + 314: "ipPayloadPacketSection", + 315: "dataLinkFrameSection", + 316: "mplsLabelStackSection", + 317: "mplsPayloadPacketSection", + 318: "selectorIdTotalPktsObserved", + 319: "selectorIdTotalPktsSelected", + 320: "absoluteError", + 321: "relativeError", + 322: "observationTimeSeconds", + 323: "observationTimeMilliseconds", + 324: "observationTimeMicroseconds", + 325: "observationTimeNanoseconds", + 326: "digestHashValue", + 327: "hashIPPayloadOffset", + 328: "hashIPPayloadSize", + 329: "hashOutputRangeMin", + 330: "hashOutputRangeMax", + 331: "hashSelectedRangeMin", + 332: "hashSelectedRangeMax", + 333: "hashDigestOutput", + 334: "hashInitialiserValue", + 335: "selectorName", + 336: "upperCILimit", + 337: "lowerCILimit", + 338: "confidenceLevel", + 339: "informationElementDataType", + 340: "informationElementDescription", + 341: "informationElementName", + 342: "informationElementRangeBegin", + 343: "informationElementRangeEnd", + 344: "informationElementSemantics", + 345: "informationElementUnits", + 346: "privateEnterpriseNumber", + 347: "virtualStationInterfaceId", + 348: "virtualStationInterfaceName", + 349: "virtualStationUUID", + 350: "virtualStationName", + 351: "layer2SegmentId", + 352: "layer2OctetDeltaCount", + 353: "layer2OctetTotalCount", + 354: "ingressUnicastPacketTotalCount", + 355: "ingressMulticastPacketTotalCount", + 356: "ingressBroadcastPacketTotalCount", + 357: "egressUnicastPacketTotalCount", + 358: "egressBroadcastPacketTotalCount", + 359: "monitoringIntervalStartMilliSeconds", + 360: "monitoringIntervalEndMilliSeconds", + 361: "portRangeStart", + 362: "portRangeEnd", + 363: "portRangeStepSize", + 364: "portRangeNumPorts", + 365: "staMacAddress", + 366: "staIPv4Address", + 367: "wtpMacAddress", + 368: "ingressInterfaceType", + 369: "egressInterfaceType", + 370: "rtpSequenceNumber", + 371: "userName", + 372: "applicationCategoryName", + 373: "applicationSubCategoryName", + 374: "applicationGroupName", + 375: "originalFlowsPresent", + 376: "originalFlowsInitiated", + 377: "originalFlowsCompleted", + 378: "distinctCountOfSourceIPAddress", + 379: "distinctCountOfDestinationIPAddress", + 380: "distinctCountOfSourceIPv4Address", + 381: "distinctCountOfDestinationIPv4Address", + 382: "distinctCountOfSourceIPv6Address", + 383: "distinctCountOfDestinationIPv6Address", + 384: "valueDistributionMethod", + 385: "rfc3550JitterMilliseconds", + 386: "rfc3550JitterMicroseconds", + 387: "rfc3550JitterNanoseconds", + 388: "dot1qDEI", + 389: "dot1qCustomerDEI", + 390: "flowSelectorAlgorithm", + 391: "flowSelectedOctetDeltaCount", + 392: "flowSelectedPacketDeltaCount", + 393: "flowSelectedFlowDeltaCount", + 394: "selectorIDTotalFlowsObserved", + 395: "selectorIDTotalFlowsSelected", + 396: "samplingFlowInterval", + 397: "samplingFlowSpacing", + 398: "flowSamplingTimeInterval", + 399: "flowSamplingTimeSpacing", + 400: "hashFlowDomain", + 401: "transportOctetDeltaCount", + 402: "transportPacketDeltaCount", + 403: "originalExporterIPv4Address", + 404: "originalExporterIPv6Address", + 405: "originalObservationDomainId", + 406: "intermediateProcessId", + 407: "ignoredDataRecordTotalCount", + 408: "dataLinkFrameType", + 409: "sectionOffset", + 410: "sectionExportedOctets", + 411: "dot1qServiceInstanceTag", + 412: "dot1qServiceInstanceId", + 413: "dot1qServiceInstancePriority", + 414: "dot1qCustomerSourceMacAddress", + 415: "dot1qCustomerDestinationMacAddress", + 417: "postLayer2OctetDeltaCount", + 418: "postMCastLayer2OctetDeltaCount", + 420: "postLayer2OctetTotalCount", + 421: "postMCastLayer2OctetTotalCount", + 422: "minimumLayer2TotalLength", + 423: "maximumLayer2TotalLength", + 424: "droppedLayer2OctetDeltaCount", + 425: "droppedLayer2OctetTotalCount", + 426: "ignoredLayer2OctetTotalCount", + 427: "notSentLayer2OctetTotalCount", + 428: "layer2OctetDeltaSumOfSquares", + 429: "layer2OctetTotalSumOfSquares", + 430: "layer2FrameDeltaCount", + 431: "layer2FrameTotalCount", + 432: "pseudoWireDestinationIPv4Address", + 433: "ignoredLayer2FrameTotalCount", + 434: "mibObjectValueInteger", + 435: "mibObjectValueOctetString", + 436: "mibObjectValueOID", + 437: "mibObjectValueBits", + 438: "mibObjectValueIPAddress", + 439: "mibObjectValueCounter", + 440: "mibObjectValueGauge", + 441: "mibObjectValueTimeTicks", + 442: "mibObjectValueUnsigned", + 443: "mibObjectValueTable", + 444: "mibObjectValueRow", + 445: "mibObjectIdentifier", + 446: "mibSubIdentifier", + 447: "mibIndexIndicator", + 448: "mibCaptureTimeSemantics", + 449: "mibContextEngineID", + 450: "mibContextName", + 451: "mibObjectName", + 452: "mibObjectDescription", + 453: "mibObjectSyntax", + 454: "mibModuleName", + 455: "mobileIMSI", + 456: "mobileMSISDN", + 457: "httpStatusCode", + 458: "sourceTransportPortsLimit", + 459: "httpRequestMethod", + 460: "httpRequestHost", + 461: "httpRequestTarget", + 462: "httpMessageVersion" +} + + +class IPFIX(Packet): + name = "IPFIX" + fields_desc = [ShortField("version", 10), + ShortField("length", None), + IntField("exportTime", None), + IntField("sequenceNumber", 1), + IntField("observationDomainID", 1)] + + +class FieldSpecifier(Packet): + name = "Field Specifier" + fields_desc = [ShortEnumField( + "informationElement", None, information_elements), + ShortField("fieldLength", None)] + + def extract_padding(self, s): + return "", s + + +class Template(Packet): + name = "Template" + fields_desc = [ShortField("templateID", 256), + FieldLenField("fieldCount", None, count_of="fields"), + PacketListField("templateFields", [], FieldSpecifier, + count_from=lambda p: p.fieldCount)] + + +class Data(Packet): + name = "Data" + fields_desc = [ + StrLenField("data", "", length_from=lambda p: p.underlayer.length - 4)] + + def extract_padding(self, s): + return "", s + + +class Set(Packet): + name = "Set" + fields_desc = [ShortField("setID", 256), + ShortField("length", None)] + + def guess_payload_class(self, payload): + if self.setID == 2: + return Template + elif self.setID > 255: + return Data + else: + return Packet.guess_payload_class(self, payload) + + +bind_layers(IPFIX, Set) +bind_layers(UDP, IPFIX, dport=4739) + + +class IPFIXDecoder(object): + """ IPFIX data set decoder """ + + def __init__(self): + self._templates = [] + + def add_template(self, template): + """ + Add IPFIX tempalte + + :param template: IPFIX template + """ + templateID = template.templateID + fields = [] + rec_len = 0 + for field in template.templateFields: + fields.append( + {'name': field.informationElement, 'len': field.fieldLength}) + rec_len += field.fieldLength + self._templates.append( + {'id': templateID, 'fields': fields, 'rec_len': rec_len}) + + def decode_data_set(self, data_set): + """ + Decode IPFIX data + + :param data_set: IPFIX data set + :returns: List of decoded data records. + """ + data = [] + for template in self._templates: + if template['id'] == data_set.setID: + offset = 0 + d = data_set[Data].data + for i in range(len(d) / template['rec_len']): + record = {} + for field in template['fields']: + f = d[offset:offset + field['len']] + offset += field['len'] + record.update({field['name']: f}) + data.append(record) + break + return data diff --git a/test/lisp.py b/test/lisp.py new file mode 100644 index 00000000..865070df --- /dev/null +++ b/test/lisp.py @@ -0,0 +1,326 @@ +from random import randint +from socket import AF_INET, AF_INET6 +from scapy.all import * +from scapy.packet import * +from scapy.fields import * +from lisp import * +from framework import * +from vpp_object import * + + +class VppLispLocatorSet(VppObject): + """ Represents LISP locator set in VPP """ + + def __init__(self, test, ls_name): + self._test = test + self._ls_name = ls_name + + @property + def test(self): + return self._test + + @property + def ls_name(self): + return self._ls_name + + def add_vpp_config(self): + self.test.vapi.lisp_locator_set(ls_name=self._ls_name) + self._test.registry.register(self, self.test.logger) + + def get_lisp_locator_sets_dump_entry(self): + result = self.test.vapi.lisp_locator_set_dump() + for ls in result: + if ls.ls_name.strip('\x00') == self._ls_name: + return ls + return None + + def query_vpp_config(self): + return self.get_lisp_locator_sets_dump_entry() is not None + + def remove_vpp_config(self): + self.test.vapi.lisp_locator_set(ls_name=self._ls_name, is_add=0) + + def object_id(self): + return 'lisp-locator-set-%s' % self._ls_name + + +class VppLispLocator(VppObject): + """ Represents LISP locator in VPP """ + + def __init__(self, test, sw_if_index, ls_name, priority=1, weight=1): + self._test = test + self._sw_if_index = sw_if_index + self._ls_name = ls_name + self._priority = priority + self._weight = weight + + @property + def test(self): + """ Test which created this locator """ + return self._test + + @property + def ls_name(self): + """ Locator set name """ + return self._ls_name + + @property + def sw_if_index(self): + return self._sw_if_index + + @property + def priority(self): + return self.priority + + @property + def weight(self): + return self._weight + + def add_vpp_config(self): + self.test.vapi.lisp_locator(ls_name=self._ls_name, + sw_if_index=self._sw_if_index, + priority=self._priority, + weight=self._weight) + self._test.registry.register(self, self.test.logger) + + def get_lisp_locator_dump_entry(self): + locators = self.test.vapi.lisp_locator_dump( + is_index_set=0, ls_name=self._ls_name) + for locator in locators: + if locator.sw_if_index == self._sw_if_index: + return locator + return None + + def query_vpp_config(self): + locator = self.get_lisp_locator_dump_entry() + return locator is not None + + def remove_vpp_config(self): + self.test.vapi.lisp_locator( + ls_name=self._ls_name, sw_if_index=self._sw_if_index, + priority=self._priority, weight=self._weight, is_add=0) + self._test.registry.register(self, self.test.logger) + + def object_id(self): + return 'lisp-locator-%s-%d' % (self._ls_name, self._sw_if_index) + + +class LispEIDType(object): + IP4 = 0 + IP6 = 1 + MAC = 2 + + +class LispKeyIdType(object): + NONE = 0 + SHA1 = 1 + SHA256 = 2 + + +class LispEID(object): + """ Lisp endpoint identifier """ + def __init__(self, eid): + self.eid = eid + + # find out whether EID is ip4 prefix, ip6 prefix or MAC + if self.eid.find("/") != -1: + if self.eid.find(":") == -1: + self.eid_type = LispEIDType.IP4 + self.data_length = 4 + else: + self.eid_type = LispEIDType.IP6 + self.data_length = 16 + + self.eid_address = self.eid.split("/")[0] + self.prefix_length = int(self.eid.split("/")[1]) + elif self.eid.count(":") == 5: # MAC address + self.eid_type = LispEIDType.MAC + self.eid_address = self.eid + self.prefix_length = 0 + self.data_length = 6 + else: + raise Exception('Unsupported EID format {}!'.format(eid)) + + def __str__(self): + if self.eid_type == LispEIDType.IP4: + return socket.inet_pton(socket.AF_INET, self.eid_address) + elif self.eid_type == LispEIDType.IP6: + return socket.inet_pton(socket.AF_INET6, self.eid_address) + elif self.eid_type == LispEIDType.MAC: + return Exception('Unimplemented') + raise Exception('Unknown EID type {}!'.format(self.eid_type)) + + +class VppLispMapping(VppObject): + """ Represents common features for remote and local LISP mapping in VPP """ + + def __init__(self, test, eid, vni=0, priority=1, weight=1): + self._eid = LispEID(eid) + self._test = test + self._priority = priority + self._weight = weight + self._vni = vni + + @property + def test(self): + return self._test + + @property + def vni(self): + return self._vni + + @property + def eid(self): + return self._eid + + @property + def priority(self): + return self._priority + + @property + def weight(self): + return self._weight + + def get_lisp_mapping_dump_entry(self): + return self.test.vapi.lisp_eid_table_dump( + eid_set=1, prefix_length=self._eid.prefix_length, + vni=self._vni, eid_type=self._eid.eid_type, eid=str(self._eid)) + + def query_vpp_config(self): + mapping = self.get_lisp_mapping_dump_entry() + return mapping + + +class VppLocalMapping(VppLispMapping): + """ LISP Local mapping """ + def __init__(self, test, eid, ls_name, vni=0, priority=1, weight=1, + key_id=LispKeyIdType.NONE, key=''): + super(VppLocalMapping, self).__init__(test, eid, vni, priority, weight) + self._ls_name = ls_name + self._key_id = key_id + self._key = key + + @property + def ls_name(self): + return self._ls_name + + @property + def key_id(self): + return self._key_id + + @property + def key(self): + return self._key + + def add_vpp_config(self): + self.test.vapi.lisp_local_mapping( + ls_name=self._ls_name, eid_type=self._eid.eid_type, + eid=str(self._eid), prefix_len=self._eid.prefix_length, + vni=self._vni, key_id=self._key_id, key=self._key) + self._test.registry.register(self, self.test.logger) + + def remove_vpp_config(self): + self.test.vapi.lisp_local_mapping( + ls_name=self._ls_name, eid_type=self._eid.eid_type, + eid=str(self._eid), prefix_len=self._eid.prefix_length, + vni=self._vni, is_add=0) + + def object_id(self): + return 'lisp-eid-local-mapping-%s[%d]' % (self._eid, self._vni) + + +class VppRemoteMapping(VppLispMapping): + + def __init__(self, test, eid, rlocs=None, vni=0, priority=1, weight=1): + super(VppRemoteMapping, self).__init__(test, eid, vni, priority, + weight) + self._rlocs = rlocs + + @property + def rlocs(self): + return self._rlocs + + def add_vpp_config(self): + self.test.vapi.lisp_remote_mapping( + rlocs=self._rlocs, eid_type=self._eid.eid_type, + eid=str(self._eid), eid_prefix_len=self._eid.prefix_length, + vni=self._vni, rlocs_num=len(self._rlocs)) + self._test.registry.register(self, self.test.logger) + + def remove_vpp_config(self): + self.test.vapi.lisp_remote_mapping( + eid_type=self._eid.eid_type, eid=str(self._eid), + eid_prefix_len=self._eid.prefix_length, vni=self._vni, + is_add=0, rlocs_num=0) + + def object_id(self): + return 'lisp-eid-remote-mapping-%s[%d]' % (self._eid, self._vni) + + +class VppLispAdjacency(VppObject): + """ Represents LISP adjacency in VPP """ + + def __init__(self, test, leid, reid, vni=0): + self._leid = LispEID(leid) + self._reid = LispEID(reid) + if self._leid.eid_type != self._reid.eid_type: + raise Exception('remote and local EID are different types!') + self._vni = vni + self._test = test + + @property + def test(self): + return self._test + + @property + def leid(self): + return self._leid + + @property + def reid(self): + return self._reid + + @property + def vni(self): + return self._vni + + def add_vpp_config(self): + self.test.vapi.lisp_adjacency( + leid=str(self._leid), + reid=str(self._reid), eid_type=self._leid.eid_type, + leid_len=self._leid.prefix_length, + reid_len=self._reid.prefix_length, vni=self._vni) + self._test.registry.register(self, self.test.logger) + + def eid_equal(self, eid, eid_type, eid_data, prefix_len): + if eid.eid_type != eid_type: + return False + + if eid_type == LispEIDType.IP4 or eid_type == LispEIDType.IP6: + if eid.prefix_length != prefix_len: + return False + + if str(eid) != eid_data[0:eid.data_length]: + return False + + return True + + def query_vpp_config(self): + res = self.test.vapi.lisp_adjacencies_get(vni=self._vni) + for adj in res.adjacencies: + if self.eid_equal(self._leid, adj.eid_type, adj.leid, + adj.leid_prefix_len) and \ + self.eid_equal(self._reid, adj.eid_type, adj.reid, + adj.reid_prefix_len): + return True + return False + + def remove_vpp_config(self): + self.test.vapi.lisp_adjacency( + leid=str(self._leid), + reid=str(self._reid), eid_type=self._leid.eid_type, + leid_len=self._leid.prefix_length, + reid_len=self._reid.prefix_length, vni=self._vni, is_add=0) + + def object_id(self): + return 'lisp-adjacency-%s-%s[%d]' % (self._leid, self._reid, self._vni) diff --git a/test/log.py b/test/log.py new file mode 100644 index 00000000..1e541d38 --- /dev/null +++ b/test/log.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import sys +import os +import logging + +""" @var formatting delimiter consisting of '=' characters """ +double_line_delim = '=' * 78 +""" @var formatting delimiter consisting of '-' characters """ +single_line_delim = '-' * 78 + + +def colorize(msg, color): + return color + msg + COLOR_RESET + + +class ColorFormatter(logging.Formatter): + + def init(self, fmt=None, datefmt=None): + super(ColorFormatter, self).__init__(fmt, datefmt) + + def format(self, record): + message = super(ColorFormatter, self).format(record) + if hasattr(record, 'color'): + message = colorize(message, record.color) + return message +try: + verbose = int(os.getenv("V", 0)) +except: + verbose = 0 + +# 40 = ERROR, 30 = WARNING, 20 = INFO, 10 = DEBUG, 0 = NOTSET (all messages) +if verbose >= 2: + log_level = 10 +elif verbose == 1: + log_level = 20 +else: + log_level = 40 + +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(ColorFormatter(fmt='%(asctime)s,%(msecs)03d %(message)s', + datefmt="%H:%M:%S")) +handler.setLevel(log_level) + +global_logger = logging.getLogger() +global_logger.addHandler(handler) + +scapy_logger = logging.getLogger("scapy.runtime") +scapy_logger.setLevel(logging.ERROR) + + +def getLogger(name): + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + return logger + +# Static variables to store color formatting strings. +# +# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure +# the color of the text to be printed in the terminal. Variable COLOR_RESET +# is used to revert the text color to the default one. +if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + LPURPLE = '\033[94m' + COLOR_RESET = '\033[0m' +else: + RED = '' + GREEN = '' + YELLOW = '' + LPURPLE = '' + COLOR_RESET = '' diff --git a/test/patches/scapy-2.3.3/dhcp6-options.patch b/test/patches/scapy-2.3.3/dhcp6-options.patch new file mode 100644 index 00000000..0e649398 --- /dev/null +++ b/test/patches/scapy-2.3.3/dhcp6-options.patch @@ -0,0 +1,58 @@ +diff --git a/scapy/layers/dhcp6.py b/scapy/layers/dhcp6.py +index 4cb9291..a1adcfc 100644 +--- a/scapy/layers/dhcp6.py ++++ b/scapy/layers/dhcp6.py +@@ -74,7 +74,9 @@ dhcp6opts = { 1: "CLIENTID", + 36: "OPTION_GEOCONF_CIVIC", #RFC-ietf-geopriv-dhcp-civil-09.txt + 37: "OPTION_REMOTE_ID", #RFC4649 + 38: "OPTION_SUBSCRIBER_ID", #RFC4580 +- 39: "OPTION_CLIENT_FQDN" } #RFC4704 ++ 39: "OPTION_CLIENT_FQDN", #RFC4704 ++ 68: "OPTION_VSS", #RFC6607 ++ 79: "OPTION_CLIENT_LINKLAYER_ADDR" } #RFC6939 + + dhcp6opts_by_code = { 1: "DHCP6OptClientId", + 2: "DHCP6OptServerId", +@@ -116,12 +118,14 @@ dhcp6opts_by_code = { 1: "DHCP6OptClientId", + #40: "DHCP6OptPANAAgent", #RFC-ietf-dhc-paa-option-05.txt + #41: "DHCP6OptNewPOSIXTimeZone, #RFC4833 + #42: "DHCP6OptNewTZDBTimeZone, #RFC4833 +- 43: "DHCP6OptRelayAgentERO" #RFC4994 ++ 43: "DHCP6OptRelayAgentERO", #RFC4994 + #44: "DHCP6OptLQQuery", #RFC5007 + #45: "DHCP6OptLQClientData", #RFC5007 + #46: "DHCP6OptLQClientTime", #RFC5007 + #47: "DHCP6OptLQRelayData", #RFC5007 + #48: "DHCP6OptLQClientLink", #RFC5007 ++ 68: "DHCP6OptVSS", #RFC6607 ++ 79: "DHCP6OptClientLinkLayerAddr", #RFC6939 + } + + +@@ -838,6 +842,26 @@ class DHCP6OptRelayAgentERO(_DHCP6OptGuessPayload): # RFC4994 + _OptReqListField("reqopts", [23, 24], + length_from = lambda pkt: pkt.optlen) ] + ++# "Client link-layer address type. The link-layer type MUST be a valid hardware ++# type assigned by the IANA, as described in [RFC0826] ++class DHCP6OptClientLinkLayerAddr(_DHCP6OptGuessPayload): #RFC6939 ++ name = "DHCP6 Option - Client Link Layer address" ++ fields_desc = [ ShortEnumField("optcode", 79, dhcp6opts), ++ FieldLenField("optlen", None, length_of="clladdr", ++ adjust = lambda pkt,x: x+1), ++ ShortField("lltype", 1), # ethernet ++ _LLAddrField("clladdr", ETHER_ANY) ] ++ ++# Virtual Subnet selection ++class DHCP6OptVSS(_DHCP6OptGuessPayload): #RFC6607 ++ name = "DHCP6 Option - Virtual Subnet Selection" ++ fields_desc = [ ShortEnumField("optcode", 68, dhcp6opts), ++ FieldLenField("optlen", None, length_of="data", ++ adjust = lambda pkt,x: x+1), ++ ByteField("type", 255), # Default Global/default table ++ StrLenField("data", "", ++ length_from = lambda pkt: pkt.optlen) ] ++ + ##################################################################### + ### DHCPv6 messages ### + ##################################################################### diff --git a/test/patches/scapy-2.3.3/gre-layers.patch b/test/patches/scapy-2.3.3/gre-layers.patch new file mode 100644 index 00000000..605a705b --- /dev/null +++ b/test/patches/scapy-2.3.3/gre-layers.patch @@ -0,0 +1,25 @@ +diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py +index 03b80ec..a7e1e0f 100644 +--- a/scapy/layers/inet6.py ++++ b/scapy/layers/inet6.py +@@ -3722,6 +3722,7 @@ conf.l2types.register(31, IPv6) + + bind_layers(Ether, IPv6, type = 0x86dd ) + bind_layers(CookedLinux, IPv6, proto = 0x86dd ) ++bind_layers(GRE, IPv6, proto = 0x86dd ) + bind_layers(IPerror6, TCPerror, nh = socket.IPPROTO_TCP ) + bind_layers(IPerror6, UDPerror, nh = socket.IPPROTO_UDP ) + bind_layers(IPv6, TCP, nh = socket.IPPROTO_TCP ) +diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py +index 4f491d2..661a5da 100644 +--- a/scapy/layers/l2.py ++++ b/scapy/layers/l2.py +@@ -628,7 +628,7 @@ bind_layers( CookedLinux, EAPOL, proto=34958) + bind_layers( GRE, LLC, proto=122) + bind_layers( GRE, Dot1Q, proto=33024) + bind_layers( GRE, Dot1AD, type=0x88a8) +-bind_layers( GRE, Ether, proto=1) ++bind_layers( GRE, Ether, proto=0x6558) + bind_layers( GRE, ARP, proto=2054) + bind_layers( GRE, EAPOL, proto=34958) + bind_layers( GRE, GRErouting, { "routing_present" : 1 } ) diff --git a/test/patches/scapy-2.3.3/inet6.py.patch b/test/patches/scapy-2.3.3/inet6.py.patch new file mode 100644 index 00000000..f98e7091 --- /dev/null +++ b/test/patches/scapy-2.3.3/inet6.py.patch @@ -0,0 +1,185 @@ +diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py +--- a/scapy/layers/inet6.py 2017-06-01 14:04:18.160881034 +0200 ++++ b/scapy/layers/inet6.py 2017-06-02 09:08:40.133800208 +0200 +@@ -369,6 +369,8 @@ + return Raw + elif self.nh == 135 and len(p) > 3: # Mobile IPv6 + return _mip6_mhtype2cls.get(ord(p[2]), MIP6MH_Generic) ++ elif self.nh == 43 and ord(p[2]) == 4: # Segment Routing header ++ return IPv6ExtHdrSegmentRouting + else: + return get_cls(ipv6nhcls.get(self.nh,"Raw"), "Raw") + +@@ -430,6 +432,14 @@ + sd = strxor(sd, a) + sd = inet_ntop(socket.AF_INET6, sd) + ++ if self.nh == 43 and isinstance(self.payload, IPv6ExtHdrSegmentRouting): ++ # With segment routing header (rh == 4), the destination is ++ # the first address of the IPv6 addresses list ++ try: ++ sd = self.addresses[0] ++ except IndexError: ++ sd = self.dst ++ + if self.nh == 44 and isinstance(self.payload, IPv6ExtHdrFragment): + nh = self.payload.nh + +@@ -489,6 +499,8 @@ + return self.payload.answers(other.payload.payload) + elif other.nh == 43 and isinstance(other.payload, IPv6ExtHdrRouting): + return self.payload.answers(other.payload.payload) # Buggy if self.payload is a IPv6ExtHdrRouting ++ elif other.nh == 43 and isinstance(other.payload, IPv6ExtHdrSegmentRouting): ++ return self.payload.answers(other.payload.payload) # Buggy if self.payload is a IPv6ExtHdrRouting + elif other.nh == 60 and isinstance(other.payload, IPv6ExtHdrDestOpt): + return self.payload.payload.answers(other.payload.payload) + elif self.nh == 60 and isinstance(self.payload, IPv6ExtHdrDestOpt): # BU in reply to BRR, for instance +@@ -919,6 +931,148 @@ + pkt = pkt[:3]+struct.pack("B", len(self.addresses))+pkt[4:] + return _IPv6ExtHdr.post_build(self, pkt, pay) + ++######################### Segment Routing Header ############################ ++ ++# This implementation is based on draft 06, available at: ++# https://tools.ietf.org/html/draft-ietf-6man-segment-routing-header-06 ++ ++class IPv6ExtHdrSegmentRoutingTLV(Packet): ++ name = "IPv6 Option Header Segment Routing - Generic TLV" ++ fields_desc = [ ByteField("type", 0), ++ ByteField("len", 0), ++ ByteField("reserved", 0), ++ ByteField("flags", 0), ++ StrLenField("value", "", length_from=lambda pkt: pkt.len) ] ++ ++ def extract_padding(self, p): ++ return "",p ++ ++ registered_sr_tlv = {} ++ @classmethod ++ def register_variant(cls): ++ cls.registered_sr_tlv[cls.type.default] = cls ++ ++ @classmethod ++ def dispatch_hook(cls, pkt=None, *args, **kargs): ++ if pkt: ++ tmp_type = ord(pkt[0]) ++ return cls.registered_sr_tlv.get(tmp_type, cls) ++ return cls ++ ++ ++class IPv6ExtHdrSegmentRoutingTLVIngressNode(IPv6ExtHdrSegmentRoutingTLV): ++ name = "IPv6 Option Header Segment Routing - Ingress Node TLV" ++ fields_desc = [ ByteField("type", 1), ++ ByteField("len", 18), ++ ByteField("reserved", 0), ++ ByteField("flags", 0), ++ IP6Field("ingress_node", "::1") ] ++ ++ ++class IPv6ExtHdrSegmentRoutingTLVEgressNode(IPv6ExtHdrSegmentRoutingTLV): ++ name = "IPv6 Option Header Segment Routing - Egress Node TLV" ++ fields_desc = [ ByteField("type", 2), ++ ByteField("len", 18), ++ ByteField("reserved", 0), ++ ByteField("flags", 0), ++ IP6Field("egress_node", "::1") ] ++ ++ ++class IPv6ExtHdrSegmentRoutingTLVPadding(IPv6ExtHdrSegmentRoutingTLV): ++ name = "IPv6 Option Header Segment Routing - Padding TLV" ++ fields_desc = [ ByteField("type", 4), ++ FieldLenField("len", None, length_of="padding", fmt="B"), ++ StrLenField("padding", b"\x00", length_from=lambda pkt: pkt.len) ] ++ ++ ++class IPv6ExtHdrSegmentRouting(_IPv6ExtHdr): ++ # 0 1 2 3 ++ # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #| Next Header | Hdr Ext Len | Routing Type | Segments Left | ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #| Last Entry | Flags | Tag | ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #| | ++ #| Segment List[0] (128 bits IPv6 address) | ++ #| | ++ #| | ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #| | ++ #| | ++ # ... ++ #| | ++ #| | ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #| | ++ #| Segment List[n] (128 bits IPv6 address) | ++ #| | ++ #| | ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ #// // ++ #// Optional Type Length Value objects (variable) // ++ #// // ++ #+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ++ # ++ # 0 1 2 3 4 5 6 7 ++ # +-+-+-+-+-+-+-+-+ ++ # |U|P|O|A|H| U | ++ # +-+-+-+-+-+-+-+-+ ++ ++ name = "IPv6 Segment Routing Extension Header" ++ fields_desc = [ ByteEnumField("nh", 59, ipv6nh), ++ ByteField("len", None), ++ ByteField("type", 4), ++ ByteField("segleft", None), ++ ByteField("lastentry", None), ++ BitField("unused1", 0, 1), ++ BitField("protected", 0, 1), ++ BitField("oam", 0, 1), ++ BitField("alert", 0, 1), ++ BitField("hmac", 0, 1), ++ BitField("unused2", 0, 3), ++ ShortField("tag", 0), ++ IP6ListField("addresses", ["::1"], ++ count_from=lambda pkt: pkt.lastentry+1), ++ PacketListField("tlv_objects", [], IPv6ExtHdrSegmentRoutingTLV, ++ length_from=lambda pkt: 8*pkt.len - 16*(pkt.lastentry+1)) ] ++ ++ overload_fields = { IPv6: { "nh": 43 } } ++ ++ def post_build(self, pkt, pay): ++ ++ if self.len is None: ++ ++ # The extension must be align on 8 bytes ++ tmp_mod = (len(pkt) - 8) % 8 ++ if tmp_mod == 1: ++ warning("IPv6ExtHdrSegmentRouting(): can't pad 1 byte !") ++ elif tmp_mod >= 2: ++ #Add the padding extension ++ tmp_pad = b"\x00" * (tmp_mod-2) ++ tlv = IPv6ExtHdrSegmentRoutingTLVPadding(padding=tmp_pad) ++ pkt += str(tlv) ++ ++ tmp_len = (len(pkt) - 8) / 8 ++ pkt = pkt[:1] + struct.pack("B", tmp_len)+ pkt[2:] ++ ++ if self.segleft is None: ++ tmp_len = len(self.addresses) ++ if tmp_len: ++ tmp_len -= 1 ++ pkt = pkt[:3] + struct.pack("B", tmp_len) + pkt[4:] ++ ++ if self.lastentry is None: ++ #km - changed to contain n-1 ++ tmp_len = len(self.addresses) ++ if tmp_len: ++ tmp_len -= 1 ++ #pkt = pkt[:4] + struct.pack("B", len(self.addresses)) + pkt[5:] ++ pkt = pkt[:4] + struct.pack("B", tmp_len) + pkt[5:] ++ ++ return _IPv6ExtHdr.post_build(self, pkt, pay) ++ ++ + ########################### Fragmentation Header ############################ + + class IPv6ExtHdrFragment(_IPv6ExtHdr): diff --git a/test/patches/scapy-2.3.3/mpls.py.patch b/test/patches/scapy-2.3.3/mpls.py.patch new file mode 100644 index 00000000..f63a70a3 --- /dev/null +++ b/test/patches/scapy-2.3.3/mpls.py.patch @@ -0,0 +1,18 @@ +diff --git a/scapy/contrib/mpls.py b/scapy/contrib/mpls.py +index 640a0c5..6af1d4a 100644 +--- a/scapy/contrib/mpls.py ++++ b/scapy/contrib/mpls.py +@@ -18,6 +18,8 @@ class MPLS(Packet): + + def guess_payload_class(self, payload): + if len(payload) >= 1: ++ if not self.s: ++ return MPLS + ip_version = (ord(payload[0]) >> 4) & 0xF + if ip_version == 4: + return IP +@@ -27,3 +29,4 @@ class MPLS(Packet): + + bind_layers(Ether, MPLS, type=0x8847) + bind_layers(GRE, MPLS, proto=0x8847) ++bind_layers(MPLS, MPLS, s=0) diff --git a/test/run_tests.py b/test/run_tests.py new file mode 100644 index 00000000..b07a923a --- /dev/null +++ b/test/run_tests.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +import sys +import shutil +import os +import select +import unittest +import argparse +from multiprocessing import Process, Pipe +from framework import VppTestRunner +from debug import spawn_gdb +from log import global_logger +from discover_tests import discover_tests + + +def test_runner_wrapper(suite, keep_alive_pipe, result_pipe): + result = not VppTestRunner( + pipe=keep_alive_pipe, + verbosity=verbose, + failfast=failfast).run(suite).wasSuccessful() + result_pipe.send(result) + result_pipe.close() + keep_alive_pipe.close() + + +class add_to_suite_callback: + def __init__(self, suite): + self.suite = suite + + def __call__(self, file_name, cls, method): + suite.addTest(cls(method)) + + +def run_forked(suite): + keep_alive_parent_end, keep_alive_child_end = Pipe(duplex=False) + result_parent_end, result_child_end = Pipe(duplex=False) + + child = Process(target=test_runner_wrapper, + args=(suite, keep_alive_child_end, result_child_end)) + child.start() + last_test_temp_dir = None + last_test_vpp_binary = None + last_test = None + result = None + while result is None: + readable = select.select([keep_alive_parent_end.fileno(), + result_parent_end.fileno(), + ], + [], [], test_timeout)[0] + if result_parent_end.fileno() in readable: + result = result_parent_end.recv() + elif keep_alive_parent_end.fileno() in readable: + while keep_alive_parent_end.poll(): + last_test, last_test_vpp_binary,\ + last_test_temp_dir, vpp_pid = keep_alive_parent_end.recv() + else: + global_logger.critical("Timeout while waiting for child test " + "runner process (last test running was " + "`%s' in `%s')!" % + (last_test, last_test_temp_dir)) + failed_dir = os.getenv('VPP_TEST_FAILED_DIR') + lttd = last_test_temp_dir.split("/")[-1] + link_path = '%s%s-FAILED' % (failed_dir, lttd) + global_logger.error("Creating a link to the failed " + + "test: %s -> %s" % (link_path, lttd)) + os.symlink(last_test_temp_dir, link_path) + api_post_mortem_path = "/tmp/api_post_mortem.%d" % vpp_pid + if os.path.isfile(api_post_mortem_path): + global_logger.error("Copying api_post_mortem.%d to %s" % + (vpp_pid, last_test_temp_dir)) + shutil.copy2(api_post_mortem_path, last_test_temp_dir) + if last_test_temp_dir and last_test_vpp_binary: + core_path = "%s/core" % last_test_temp_dir + if os.path.isfile(core_path): + global_logger.error("Core-file exists in test temporary " + "directory: %s!" % core_path) + if d and d.lower() == "core": + spawn_gdb(last_test_vpp_binary, core_path, + global_logger) + child.terminate() + result = -1 + keep_alive_parent_end.close() + result_parent_end.close() + return result + + +if __name__ == '__main__': + + try: + verbose = int(os.getenv("V", 0)) + except: + verbose = 0 + + default_test_timeout = 600 # 10 minutes + try: + test_timeout = int(os.getenv("TIMEOUT", default_test_timeout)) + except: + test_timeout = default_test_timeout + + try: + debug = os.getenv("DEBUG") + except: + debug = None + + parser = argparse.ArgumentParser(description="VPP unit tests") + parser.add_argument("-f", "--failfast", action='count', + help="fast failure flag") + parser.add_argument("-d", "--dir", action='append', type=str, + help="directory containing test files " + "(may be specified multiple times)") + args = parser.parse_args() + failfast = True if args.failfast == 1 else False + + suite = unittest.TestSuite() + cb = add_to_suite_callback(suite) + for d in args.dir: + global_logger.info("Adding tests from directory tree %s" % d) + discover_tests(d, cb) + + if debug is None or debug.lower() not in ["gdb", "gdbserver"]: + sys.exit(run_forked(suite)) + + # don't fork if debugging.. + sys.exit(not VppTestRunner(verbosity=verbose, + failfast=failfast).run(suite).wasSuccessful()) diff --git a/test/sanity_import_vpp_papi.py b/test/sanity_import_vpp_papi.py new file mode 100644 index 00000000..535e00c9 --- /dev/null +++ b/test/sanity_import_vpp_papi.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +""" sanity check script """ +import vpp_papi diff --git a/test/sanity_run_vpp.py b/test/sanity_run_vpp.py new file mode 100644 index 00000000..156608c1 --- /dev/null +++ b/test/sanity_run_vpp.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +from __future__ import print_function +from multiprocessing import Pipe +from sys import exit +from hook import VppDiedError +from framework import VppTestCase, KeepAliveReporter + + +class SanityTestCase(VppTestCase): + """ Dummy test case used to check if VPP is able to start """ + pass + +if __name__ == '__main__': + rc = 0 + tc = SanityTestCase + x, y = Pipe() + reporter = KeepAliveReporter() + reporter.pipe = y + try: + tc.setUpClass() + except VppDiedError: + rc = -1 + else: + try: + tc.tearDownClass() + except: + pass + x.close() + y.close() + + exit(rc) diff --git a/test/scapy_handlers/__init__.py b/test/scapy_handlers/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/scapy_handlers/__init__.py diff --git a/test/scripts/compress_failed.sh b/test/scripts/compress_failed.sh new file mode 100755 index 00000000..9559e2ac --- /dev/null +++ b/test/scripts/compress_failed.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +if [ "$(ls -A ${VPP_TEST_FAILED_DIR})" ] +then + if [ "${COMPRESS_FAILED_TEST_LOGS}" == "yes" ] + then + echo -n "Compressing files in temporary directories from failed test runs... " + cd ${VPP_TEST_FAILED_DIR} + for d in * + do + cd ${d} + find . ! -path . -print0 | xargs -0 -n1 gzip + cd ${VPP_TEST_FAILED_DIR} + done + echo "done." + if [ -n "$WORKSPACE" ] + then + echo "Copying failed test logs into build log archive directory ($WORKSPACE/archives)... " + for failed_test in $(ls $VPP_TEST_FAILED_DIR) + do + mkdir -p $WORKSPACE/archives/$failed_test + cp -a $VPP_TEST_FAILED_DIR/$failed_test/* $WORKSPACE/archives/$failed_test + done + echo "done." + fi + + else + echo "Not compressing files in temporary directories from failed test runs." + fi +else + echo "No symlinks to failed tests' temporary directories found in ${VPP_TEST_FAILED_DIR}." +fi + +# This script gets run only if there was a 'make test' failure, +# so return failure error status so that the build results are +# recorded correctly. +exit 1 diff --git a/test/scripts/git_pull_or_clean.sh b/test/scripts/git_pull_or_clean.sh new file mode 100755 index 00000000..b119a9cc --- /dev/null +++ b/test/scripts/git_pull_or_clean.sh @@ -0,0 +1,8 @@ +#!/bin/sh +CMD='git clean -dfX */' + +if git pull | grep -v 'Already up-to-date.' +then + echo "Executing $CMD" + $CMD +fi diff --git a/test/scripts/run_in_venv_with_cleanup.sh b/test/scripts/run_in_venv_with_cleanup.sh new file mode 100755 index 00000000..35b6737e --- /dev/null +++ b/test/scripts/run_in_venv_with_cleanup.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +rv=0 + +atexit() { + group_id=`ps -p $$ -o pgid=` + my_id=$$ + ids=`pgrep -g $group_id -d ' ' | sed "s/\b$my_id\b//g"` + echo "Killing possible remaining process IDs: $ids" + for id in $ids + do + if ps -p $id > /dev/null + then + kill -9 $id + fi + done + exit ${rv} +} + +trap "atexit;" SIGINT SIGTERM + +FORCE_FOREGROUND=$1 +shift + +source $1 +shift + +if [[ "${FORCE_FOREGROUND}" == "1" ]] +then + $* +else + $* & + pid=$! + wait ${pid} +fi + +rv=$? +atexit +exit ${rv} diff --git a/test/scripts/setsid_wrapper.sh b/test/scripts/setsid_wrapper.sh new file mode 100755 index 00000000..6d63426b --- /dev/null +++ b/test/scripts/setsid_wrapper.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [[ "$1" == "1" ]] +then + setsid scripts/run_in_venv_with_cleanup.sh $* +else + setsid scripts/run_in_venv_with_cleanup.sh $* & + pid=$! + trap "echo setsid_wrapper.sh: got signal, killing child pid ${pid}; kill ${pid}; sleep .1;" SIGINT SIGTERM + wait ${pid} + exit $? +fi diff --git a/test/scripts/socket_test.sh b/test/scripts/socket_test.sh new file mode 100755 index 00000000..39454557 --- /dev/null +++ b/test/scripts/socket_test.sh @@ -0,0 +1,855 @@ +#! /bin/bash +# +# socket_test.sh -- script to run socket tests. +# +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +vpp_dir="$WS_ROOT/build-root/install-vpp-native/vpp/bin/" +vpp_debug_dir="$WS_ROOT/build-root/install-vpp_debug-native/vpp/bin/" +vpp_shm_dir="/dev/shm/" +vpp_run_dir="/run/vpp" +lib64_dir="$WS_ROOT/build-root/install-vpp-native/vpp/lib64/" +lib64_debug_dir="$WS_ROOT/build-root/install-vpp_debug-native/vpp/lib64/" +dpdk_devbind="/usr/share/dpdk/usertools/dpdk-devbind.py" +docker_vpp_dir="/vpp/" +docker_app_dir="/vpp/" +docker_lib64_dir="/vpp-lib64/" +docker_os="ubuntu" +vcl_ldpreload_lib="libvcl_ldpreload.so.0.0.0" +user_gid="$(id -g)" +vpp_app="vpp" +sock_srvr_app="sock_test_server" +sock_clnt_app="sock_test_client" +sock_srvr_addr="127.0.0.1" +sock_srvr_port="22000" +iperf_srvr_app="iperf3 -V4d -s" +iperf_clnt_app="iperf3 -V4d -c \$srvr_addr" +gdb_in_emacs="gdb_in_emacs" +vppcom_conf="vppcom.conf" +vppcom_conf_dir="$WS_ROOT/src/uri/" +docker_vppcom_conf_dir="/etc/vpp/" +xterm_geom="100x60" +bash_header="#! /bin/bash" +tmp_cmdfile_prefix="/tmp/socket_test_cmd" +cmd1_file="${tmp_cmdfile_prefix}1.$$" +cmd2_file="${tmp_cmdfile_prefix}2.$$" +cmd3_file="${tmp_cmdfile_prefix}3.$$" +vpp_eth_name="enp0s8" +tmp_vpp_exec_file="/tmp/vpp_config.$$" +tmp_gdb_cmdfile_prefix="/tmp/gdb_cmdfile" +def_gdb_cmdfile_prefix="$WS_ROOT/extras/gdb/gdb_cmdfile" +tmp_gdb_cmdfile_vpp="${tmp_gdb_cmdfile_prefix}_vpp.$$" +tmp_gdb_cmdfile_client="${tmp_gdb_cmdfile_prefix}_vcl_client.$$" +tmp_gdb_cmdfile_server="${tmp_gdb_cmdfile_prefix}_vcl_server.$$" +get_docker_server_ip4addr='srvr_addr=$(docker network inspect bridge | grep IPv4Address | awk -e '\''{print $2}'\'' | sed -e '\''s,/16,,'\'' -e '\''s,",,g'\'' -e '\''s/,//'\'')' +#' single quote to fix the confused emacs colorizer. +trap_signals="SIGINT SIGTERM EXIT" + +# Set default values for imported environment variables if they don't exist. +# +VPP_GDB_CMDFILE="${VPP_GDB_CMDFILE:-${def_gdb_cmdfile_prefix}.vpp}" +VPPCOM_CLIENT_GDB_CMDFILE="${VPPCOM_CLIENT_GDB_CMDFILE:-${def_gdb_cmdfile_prefix}.vppcom_client}" +VPPCOM_SERVER_GDB_CMDFILE="${VPPCOM_SERVER_GDB_CMDFILE:-${def_gdb_cmdfile_prefix}.vppcom_server}" +VCL_LDPRELOAD_LIB_DIR="${VCL_LDPRELOAD_LIB_DIR:-/usr/local/lib}" + +usage() { + cat <<EOF +Usage: socket_test.sh OPTIONS TEST +TESTS: + nk, native-kernel Run server & client on host using kernel. + nv, native-vcl Run vpp, server & client on host using VppComLib. + np, native-preload Run vpp, server & client on host using LD_PRELOAD. + dk, docker-kernel Run server & client in docker using kernel stack. + dv, docker-vcl Run vpp on host, server & client in docker using VppComLib. + dp, docker-preload Run vpp on host, server & client in docker using LD_PRELOAD. + +OPTIONS: + -h Print this usage text. + -l Leave ${tmp_cmdfile_prefix}* files after test run. + -b Run bash after application exit. + -d Run the vpp_debug version of all apps. + -c Set VPPCOM_CONF to use the vppcom_test.conf file. + -i Run iperf3 for client/server app in native tests. + -n Name of ethernet for VPP to use in multi-host cfg. + -6 Use ipv6 addressing. + -m c[lient] Run client in multi-host cfg (server on remote host) + s[erver] Run server in multi-host cfg (client on remote host) + -e a[ll] Run all in emacs+gdb. + c[lient] Run client in emacs+gdb. + s[erver] Run server in emacs+gdb. + v[pp] Run vpp in emacs+gdb. + -g a[ll] Run all in gdb. + c[lient] Run client in gdb. + s[erver] Run server in gdb. + v[pp] Run vpp in gdb. + -t Use tabs in one xterm if available (e.g. xfce4-terminal). + +OPTIONS passed to client/server: + -S <ip address> Server IP address. + -P <server port> Server Port number. + -E <data> Run Echo test. + -N <num-writes> Test Cfg: number of writes. + -R <rxbuf-size> Test Cfg: rx buffer size. + -T <txbuf-size> Test Cfg: tx buffer size. + -U Run Uni-directional test. + -B Run Bi-directional test. + -I <num-tst-socks> Send data over multiple test sockets in parallel. + -V Test Cfg: Verbose mode. + -X Exit client/server after running test. + +Environment variables: + VPPCOM_CONF Pathname of vppcom configuration file. + VPP_GDB_CMDFILE Pathname of gdb command file for vpp. + VPPCOM_CLIENT_GDB_CMDFILE Pathname of gdb command file for client. + VPPCOM_SERVER_GDB_CMDFILE Pathname of gdb command file for server. +EOF + exit 1 +} + +declare -i emacs_vpp=0 +declare -i emacs_client=0 +declare -i emacs_server=0 +declare -i gdb_vpp=0 +declare -i gdb_client=0 +declare -i gdb_server=0 +declare -i perf_vpp=0 +declare -i perf_client=0 +declare -i perf_server=0 +declare -i leave_tmp_files=0 +declare -i bash_after_exit=0 +declare -i iperf3=0 +declare -i use_ipv6=0 + +while getopts ":hitlbcd6n:m:e:g:p:E:I:N:P:R:S:T:UBVX" opt; do + case $opt in + h) usage ;; + l) leave_tmp_files=1 + ;; + b) bash_after_exit=1 + ;; + i) iperf3=1 + ;; + 6) use_ipv6=1 + ;; + t) xterm_geom="180x40" + use_tabs="true" + ;; + c) VPPCOM_CONF="${vppcom_conf_dir}vppcom_test.conf" + ;; + d) title_dbg="-DEBUG" + _debug="_debug" + vpp_dir=$vpp_debug_dir + lib64_dir=$lib64_debug_dir + ;; + e) if [ $OPTARG = "a" ] || [ $OPTARG = "all" ] ; then + emacs_client=1 + emacs_server=1 + emacs_vpp=1 + elif [ $OPTARG = "c" ] || [ $OPTARG = "client" ] ; then + emacs_client=1 + elif [ $OPTARG = "s" ] || [ $OPTARG = "server" ] ; then + emacs_server=1 + elif [ $OPTARG = "v" ] || [ $OPTARG = "vpp" ] ; then + emacs_vpp=1 + else + echo "ERROR: Option -e unknown argument \'$OPTARG\'" >&2 + usage + fi + title_dbg="-DEBUG" + vpp_dir=$vpp_debug_dir + lib64_dir=$lib64_debug_dir + ;; + n) vpp_eth_name="$OPTARG" + ;; + m) if [ $OPTARG = "c" ] || [ $OPTARG = "client" ] ; then + multi_host="client" + elif [ $OPTARG = "s" ] || [ $OPTARG = "server" ] ; then + multi_host="server" + else + echo "ERROR: Option -e unknown argument \'$OPTARG\'" >&2 + usage + fi + ;; + g) if [ $OPTARG = "a" ] || [ $OPTARG = "all" ] ; then + gdb_client=1 + gdb_server=1 + gdb_vpp=1 + elif [ $OPTARG = "c" ] || [ $OPTARG = "client" ] ; then + gdb_client=1 + elif [ $OPTARG = "s" ] || [ $OPTARG = "server" ] ; then + gdb_server=1 + elif [ $OPTARG = "v" ] || [ $OPTARG = "vpp" ] ; then + gdb_vpp=1 + else + echo "ERROR: Option -g unknown argument \'$OPTARG\'" >&2 + usage + fi + title_dbg="-DEBUG" + vpp_dir=$vpp_debug_dir + lib64_dir=$lib64_debug_dir + ;; + p) if [ $OPTARG = "a" ] || [ $OPTARG = "all" ] ; then + perf_client=1 + perf_server=1 + perf_vpp=1 + elif [ $OPTARG = "c" ] || [ $OPTARG = "client" ] ; then + perf_client=1 + elif [ $OPTARG = "s" ] || [ $OPTARG = "server" ] ; then + perf_server=1 + elif [ $OPTARG = "v" ] || [ $OPTARG = "vpp" ] ; then + perf_vpp=1 + else + echo "ERROR: Option -p unknown argument \'$OPTARG\'" >&2 + usage + fi + echo "WARNING: -p options TBD" + ;; + S) sock_srvr_addr="$OPTARG" + ;; + P) sock_srvr_port="$OPTARG" + ;; +E|I|N|R|T) sock_clnt_options="$sock_clnt_options -$opt \"$OPTARG\"" + ;; + U|B|V|X) sock_clnt_options="$sock_clnt_options -$opt" + ;; + \?) + echo "ERROR: Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "ERROR: Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +shift $(( $OPTIND-1 )) +while ! [[ $run_test ]] && (( $# > 0 )) ; do + case $1 in + "nk" | "native-kernel") + run_test="native_kernel" ;; + "np" | "native-preload") + run_test="native_preload" ;; + "nv" | "native-vcl") + sock_srvr_app="vcl_test_server" + sock_clnt_app="vcl_test_client" + run_test="native_vcl" ;; + "dk" | "docker-kernel") + run_test="docker_kernel" ;; + "dp" | "docker-preload") + run_test="docker_preload" ;; + "dv" | "docker-vcl") + sock_srvr_app="vcl_test_server" + sock_clnt_app="vcl_test_client" + run_test="docker_vcl" ;; + *) + echo "ERROR: Unknown option '$1'!" >&2 + usage ;; + esac + shift +done + +if [ -z "$WS_ROOT" ] ; then + echo "ERROR: WS_ROOT environment variable not set!" >&2 + echo " Please set WS_ROOT to VPP workspace root directory." >&2 + exit 1 +fi + +if [[ "$(grep bin_PROGRAMS $WS_ROOT/src/uri.am)" = "" ]] ; then + $WS_ROOT/extras/vagrant/vcl_test.sh $WS_ROOT $USER + (cd $WS_ROOT; make build) +fi + +if [ ! -d $vpp_dir ] ; then + echo "ERROR: Missing VPP$DEBUG bin directory!" >&2 + echo " $vpp_dir" >&2 + env_test_failed="true" +fi + +if [[ $run_test =~ .*"_preload" ]] ; then + if [ ! -d $lib64_dir ] ; then + echo "ERROR: Missing VPP$DEBUG lib64 directory!" >&2 + echo " $lib64_dir" >&2 + elif [ ! -d $VCL_LDPRELOAD_LIB_DIR ] ; then + echo "ERROR: Missing VCL LD_PRELOAD Library directory!" >&2 + echo " $VCL_LDPRELOAD_LIB_DIR" >&2 + env_test_failed="true" + elif [ ! -f $VCL_LDPRELOAD_LIB_DIR/$vcl_ldpreload_lib ] ; then + echo "ERROR: Missing VCL LD_PRELOAD library!" >&2 + echo " $VCL_LDPRELOAD_LIB_DIR/$vcl_ldpreload_lib" >&2 + env_test_failed="true" + fi +fi + +if [ ! -f $vpp_dir$vpp_app ] ; then + echo "ERROR: Missing VPP$DEBUG Application!" >&2 + echo " $vpp_dir$vpp_app" >&2 + env_test_failed="true" +fi + +if [ ! -f $vpp_dir$sock_srvr_app ] && [ ! $iperf3 -eq 1 ] ; then + echo "ERROR: Missing$DEBUG Socket Server Application!" >&2 + echo " $vpp_dir$sock_srvr_app" >&2 + env_test_failed="true" +fi + +if [ ! -f $vpp_dir$sock_clnt_app ] && [ ! $iperf3 -eq 1 ] ; then + echo "ERROR: Missing$DEBUG Socket Client Application!" >&2 + echo " $vpp_dir$sock_clnt_app" >&2 + env_test_failed="true" +fi + +if [[ $run_test =~ "docker_".* ]] ; then + if [ $emacs_client -eq 1 ] || [ $emacs_server -eq 1 ] || [ $gdb_client -eq 1 ] || [ $gdb_server -eq 1 ] ; then + + echo "WARNING: gdb is not currently supported in docker." + echo " Ignoring client/server gdb options." + emacs_client=0 + emacs_server=0 + gdb_client=0 + gdb_server=0 + fi +fi + +if [[ $run_test =~ .*"_vcl" ]] && [ $iperf3 -eq 1 ] ; then + echo "ERROR: Invalid option 'i' for test $run_test!" + echo " iperf3 is not compiled with the VCL library." + env_test_failed="true" +fi + +if [ -n "$mult_host"] && [ ! -f "$dpdk_devbind" ] ; then + echo "ERROR: Can't find dpdk-devbind.py!" + echo " Run \"cd \$WS_ROOT; make dpdk-install-dev\" to install it." + echo + env_test_failed="true" +fi + +if [ -n "$env_test_failed" ] ; then + exit 1 +fi + +if [ -f "$VPPCOM_CONF" ] ; then + vppcom_conf="$(basename $VPPCOM_CONF)" + vppcom_conf_dir="$(dirname $VPPCOM_CONF)/" + api_prefix="$(egrep -s '^\s*api-prefix \w+' $VPPCOM_CONF | awk -e '{print $2}')" + if [ -n "$api_prefix" ] ; then + api_segment=" api-segment { gid $user_gid prefix $api_prefix }" + fi +fi +if [ -z "$api_segment" ] ; then + api_segment=" api-segment { gid $user_gid }" +fi +if [ -n "$multi_host" ] ; then + sudo modprobe uio_pci_generic + vpp_args="unix { interactive exec $tmp_vpp_exec_file}${api_segment}" +else + vpp_args="unix { interactive }${api_segment}" +fi + +if [ $iperf3 -eq 1 ] ; then + app_dir="$(dirname $(which iperf3))/" + srvr_app=$iperf_srvr_app + clnt_app=$iperf_clnt_app + if [[ $run_test =~ "docker_".* ]] ; then + unset -v app_dir + sock_srvr_port=5201 + docker_app_dir="networkstatic/" + unset -v docker_os + fi +else + app_dir="$vpp_dir" + srvr_app="$sock_srvr_app $sock_srvr_port" + clnt_app="$sock_clnt_app${sock_clnt_options} \$srvr_addr $sock_srvr_port" +fi + +verify_no_vpp() { + local grep_for_vpp="ps -eaf|grep -v grep|grep \"bin/vpp\"" + + if [ -n "$api_prefix" ] ; then + grep_for_vpp="$grep_for_vpp|grep \"prefix $api_prefix\"" + fi + local running_vpp="$(eval $grep_for_vpp)" + if [ -n "$running_vpp" ] ; then + echo "ERROR: Please kill the following vpp instance(s):" + echo + echo $running_vpp + echo + exit 1 + fi + clean_devshm="$vpp_shm_dir*db $vpp_shm_dir*global_vm $vpp_shm_dir*vpe-api $vpp_shm_dir[0-9]*-[0-9]* $vpp_shm_dir*:segment[0-9]*" + sudo rm -f $clean_devshm + devshm_files="$(ls -l $clean_devshm 2>/dev/null | grep $(whoami))" + if [ "$devshm_files" != "" ] ; then + echo "ERROR: Please remove the following $vpp_shm_dir files:" + for file in "$devshm_files" ; do + echo " $file" + done + exit 1 + fi + if [ ! -d "$vpp_run_dir" ] ; then + sudo mkdir $vpp_run_dir + sudo chown root:$USER $vpp_run_dir + fi + if [ -n "$multi_host" ] ; then + vpp_eth_pci_id="$(ls -ld /sys/class/net/$vpp_eth_name/device | awk '{print $11}' | cut -d/ -f4)" + if [ -z "$vpp_eth_pci_id" ] ; then + echo "ERROR: Missing ethernet interface $vpp_eth_name!" + usage + fi + printf -v bus "%x" "0x$(echo $vpp_eth_pci_id | cut -d: -f2)" + printf -v slot "%x" "0x$(echo $vpp_eth_pci_id | cut -d: -f3 | cut -d. -f1)" + printf -v func "%x" "0x$(echo $vpp_eth_pci_id | cut -d. -f2)" + + vpp_eth_kernel_driver="$(basename $(ls -l /sys/bus/pci/devices/$vpp_eth_pci_id/driver | awk '{print $11}'))" + if [ -z "$vpp_eth_kernel_driver" ] ; then + echo "ERROR: Missing kernel driver for $vpp_eth_name!" + usage + fi + case $vpp_eth_kernel_driver in + e1000) + vpp_eth_ifname="GigabitEthernet$bus/$slot/$func" ;; + ixgbe) + vpp_eth_ifname="TenGigabitEthernet$bus/$slot/$func" ;; + i40e) + vpp_eth_ifname="FortyGigabitEthernet$bus/$slot/$func" ;; + *) + echo "ERROR: Unknown ethernet kernel driver $vpp_eth_kernel_driver!" + usage ;; + esac + + vpp_eth_ip4_addr="$(ip -4 -br addr show $vpp_eth_name | awk '{print $3}')" + if [ -z "$vpp_eth_ip4_addr" ] ; then + if [ "$multi_host" = "server" ] ; then + vpp_eth_ip4_addr="10.10.10.10/24" + else + vpp_eth_ip4_addr="10.10.10.11/24" + fi + fi + if [ $use_ipv6 -eq 1 ] && [ -z "$vpp_eth_ip6_addr" ] ; then + echo "ERROR: No inet6 address configured for $vpp_eth_name!" + usage + fi + vpp_args="$vpp_args plugins { path ${lib64_dir}vpp_plugins } dpdk { dev $vpp_eth_pci_id }" + + sudo ifconfig $vpp_eth_name down 2> /dev/null + echo "Configuring VPP to use $vpp_eth_name ($vpp_eth_pci_id), inet addr $vpp_eth_ip4_addr" + + cat <<EOF >> $tmp_vpp_exec_file +set int state $vpp_eth_ifname up +set int ip addr $vpp_eth_ifname $vpp_eth_ip4_addr +EOF + + fi +} + +verify_no_docker_containers() { + if (( $(which docker | wc -l) < 1 )) ; then + echo "ERROR: docker is not installed!" + echo "See https://docs.docker.com/engine/installation/linux/ubuntu/" + echo " or https://docs.docker.com/engine/installation/linux/centos/" + exit 1 + fi + if (( $(docker ps | wc -l) > 1 )) ; then + echo "ERROR: Run the following to kill all docker containers:" + echo "docker kill \$(docker ps -q)" + echo + docker ps + exit 1 + fi +} + +set_pre_cmd() { + # arguments + # $1 : emacs flag + # $2 : gdb flag + # $3 : optional LD_PRELOAD library pathname + local -i emacs=$1 + local -i gdb=$2 + + if [ $emacs -eq 1 ] ; then + write_gdb_cmdfile $tmp_gdb_cmdfile $gdb_cmdfile $emacs $3 + pre_cmd="$gdb_in_emacs " + elif [ $gdb -eq 1 ] ; then + write_gdb_cmdfile $tmp_gdb_cmdfile $gdb_cmdfile $emacs $3 + pre_cmd="gdb -x $tmp_gdb_cmdfile -i=mi --args " + elif [ -z $3 ] ; then + unset -v pre_cmd + else + docker_ld_preload="-e LD_PRELOAD=$3 " + pre_cmd="LD_PRELOAD=$3 " + fi +} + +write_script_header() { + # arguments + # $1 : command script file + # $2 : gdb command file + # $3 : title + # $4 : optional command string (typically "sleep 2") + echo "$bash_header" > $1 + echo -e "#\n# $1 generated on $(date)\n#" >> $1 + if [ $leave_tmp_files -eq 0 ] ; then + if [ -n "$multi_host" ] ; then + echo "trap \"rm -f $1 $2 $tmp_vpp_exec_file; sudo $dpdk_devbind -b $vpp_eth_kernel_driver $vpp_eth_pci_id; sudo ifconfig $vpp_eth_name up\" $trap_signals" >> $1 + else + echo "trap \"rm -f $1 $2 $tmp_vpp_exec_file\" $trap_signals" >> $1 + fi + fi + echo "export VPPCOM_CONF=${vppcom_conf_dir}${vppcom_conf}" >> $1 + if [ "$pre_cmd" = "$gdb_in_emacs " ] ; then + if [ -n "$multi_host" ] && [[ $3 =~ "VPP".* ]] ; then + cat <<EOF >> $1 +$gdb_in_emacs() { + sudo emacs --eval "(gdb \"gdb -x $2 -i=mi --args \$*\")" --eval "(setq frame-title-format \"$3\")" +} +EOF + else + cat <<EOF >> $1 +$gdb_in_emacs() { + emacs --eval "(gdb \"gdb -x $2 -i=mi --args \$*\")" --eval "(setq frame-title-format \"$3\")" +} +EOF + fi + fi + if [ -n "$4" ] ; then + echo "$4" >> $1 + fi +} + +write_script_footer() { + # arguments + # $1 : command script file + # $2 : perf flag indicating to run bash before exit + local -i perf=$2 + if [ $bash_after_exit -eq 1 ] || [ $perf -eq 1 ] ; then + echo "bash" >> $1 + fi +} + +write_gdb_cmdfile() { + # arguments + # $1 : gdb command file + # $2 : User specified gdb cmdfile + # $3 : emacs flag + # $4 : optional LD_PRELOAD library pathname. + local -i emacs=$3 + + echo "# $1 generated on $(date)" > $1 + echo "#" >> $1 + echo "set confirm off" >> $1 + if [ -n "$4" ] ; then + echo "set exec-wrapper env LD_PRELOAD=$4" >> $1 + echo "start" >> $1 + fi + + if [ ! -f $2 ] ; then + echo -n "# " >> $1 + fi + echo "source $2" >> $1 + if [ $emacs -eq 0 ] ; then + echo "run" >> $1 + fi +} + +native_kernel() { + banner="Running NATIVE-KERNEL socket test" + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title1="SERVER$title_dbg (Native-Kernel Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + echo "${pre_cmd}${app_dir}${srvr_app}" >> $cmd1_file + write_script_footer $cmd1_file $perf_server + chmod +x $cmd1_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title2="CLIENT$title_dbg (Native-Kernel Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "sleep 2" + echo "srvr_addr=\"$sock_srvr_addr\"" >> $cmd2_file + echo "${pre_cmd}${app_dir}${clnt_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_client + chmod +x $cmd2_file + + fi +} + +native_preload() { + verify_no_vpp + banner="Running NATIVE-PRELOAD socket test" + ld_preload="$VCL_LDPRELOAD_LIB_DIR/$vcl_ldpreload_lib " + + title1="VPP$title_dbg (Native-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_vpp + gdb_cmdfile=$VPP_GDB_CMDFILE + set_pre_cmd $emacs_vpp $gdb_vpp + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + if [ -n "$multi_host" ] && [ $emacs_vpp -eq 0 ] ; then + echo -n "sudo " >> $cmd1_file + fi + echo "${pre_cmd}$vpp_dir$vpp_app $vpp_args " >> $cmd1_file + write_script_footer $cmd1_file $perf_vpp + chmod +x $cmd1_file + + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title2="SERVER$title_dbg (Native-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server $ld_preload + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "sleep 2" + echo "export LD_LIBRARY_PATH=\"$lib64_dir:$VCL_LDPRELOAD_LIB_DIR:$LD_LIBRARY_PATH\"" >> $cmd2_file + echo "${pre_cmd}${app_dir}${srvr_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_server + chmod +x $cmd2_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title3="CLIENT$title_dbg (Native-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client $ld_preload + write_script_header $cmd3_file $tmp_gdb_cmdfile "$title3" "sleep 3" + echo "export LD_LIBRARY_PATH=\"$lib64_dir:$VCL_LDPRELOAD_LIB_DIR:$LD_LIBRARY_PATH\"" >> $cmd3_file + echo "srvr_addr=\"$sock_srvr_addr\"" >> $cmd3_file + echo "${pre_cmd}${app_dir}${clnt_app}" >> $cmd3_file + write_script_footer $cmd3_file $perf_client + chmod +x $cmd3_file + fi +} + +native_vcl() { + verify_no_vpp + banner="Running NATIVE-VCL socket test" + + title1="VPP$title_dbg (Native-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_vpp + gdb_cmdfile=$VPP_GDB_CMDFILE + set_pre_cmd $emacs_vpp $gdb_vpp + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + if [ -n "$multi_host" ] && [ $emacs_vpp -eq 0 ] ; then + echo -n "sudo " >> $cmd1_file + fi + echo "${pre_cmd}$vpp_dir$vpp_app $vpp_args " >> $cmd1_file + write_script_footer $cmd1_file $perf_vpp + chmod +x $cmd1_file + + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title2="SERVER$title_dbg (Native-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server + if [ "$multi_host" = "server" ] ; then + delay="sleep 10" + else + delay="sleep 3" + fi + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "$delay" + echo "export LD_LIBRARY_PATH=\"$lib64_dir:$LD_LIBRARY_PATH\"" >> $cmd2_file + echo "${pre_cmd}${app_dir}${srvr_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_server + chmod +x $cmd2_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title3="CLIENT$title_dbg (Native-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client + if [ "$multi_host" = "client" ] ; then + delay="sleep 10" + else + delay="sleep 3" + fi + write_script_header $cmd3_file $tmp_gdb_cmdfile "$title3" "$delay" + echo "export LD_LIBRARY_PATH=\"$lib64_dir:$LD_LIBRARY_PATH\"" >> $cmd3_file + echo "srvr_addr=\"$sock_srvr_addr\"" >> $cmd3_file + echo "${pre_cmd}${app_dir}${clnt_app}" >> $cmd3_file + write_script_footer $cmd3_file $perf_client + chmod +x $cmd3_file + fi +} + +docker_kernel() { + verify_no_docker_containers + banner="Running DOCKER-KERNEL socket test" + + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title1="SERVER$title_dbg (Docker-Native Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + echo "docker run -it --cpuset-cpus='4-7' --cpuset-cpus='4-7' -v $vpp_dir:$docker_vpp_dir -p $sock_srvr_port:$sock_srvr_port $docker_os ${docker_app_dir}${srvr_app}" >> $cmd1_file + write_script_footer $cmd1_file $perf_server + chmod +x $cmd1_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title2="CLIENT$title_dbg (Docker-Native Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "sleep 2" + echo "$get_docker_server_ip4addr" >> $cmd2_file + echo "docker run -it --cpuset-cpus='4-7' -v $vpp_dir:$docker_vpp_dir $docker_os ${docker_app_dir}${clnt_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_client + chmod +x $cmd2_file + fi +} + +docker_preload() { + verify_no_vpp + verify_no_docker_containers + banner="Running DOCKER-PRELOAD socket test" + docker_ld_preload_dir="/vcl-ldpreload/" + ld_preload_dir="$VCL_LDPRELOAD_LIB_DIR" + ld_preload="$docker_ld_preload_dir$vcl_ldpreload_lib " + docker_ld_preload_lib="$docker_ld_preload_dir$vcl_ldpreload_lib " + + title1="VPP$title_dbg (Docker-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_vpp + gdb_cmdfile=$VPP_GDB_CMDFILE + set_pre_cmd $emacs_vpp $gdb_vpp + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + if [ -n "$multi_host" ] ; then + echo -n "sudo " >> $cmd1_file + fi + echo "${pre_cmd}$vpp_dir$vpp_app $vpp_args" >> $cmd1_file + write_script_footer $cmd1_file $perf_vpp + chmod +x $cmd1_file + + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title2="SERVER$title_dbg (Docker-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server $docker_ld_preload_lib + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "sleep 2" + echo "docker run -it -v $vpp_shm_dir:$vpp_shm_dir -v $vpp_dir:$docker_vpp_dir -v $lib64_dir:$docker_lib64_dir -v $ld_preload_dir:$docker_ld_preload_dir -v $vppcom_conf_dir:$docker_vppcom_conf_dir -p $sock_srvr_port:$sock_srvr_port -e VPPCOM_CONF=${docker_vppcom_conf_dir}$vppcom_conf -e LD_LIBRARY_PATH=$docker_lib64_dir:$docker_ld_preload_dir ${docker_ld_preload}$docker_os ${docker_app_dir}${srvr_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_server + chmod +x $cmd2_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title3="CLIENT$title_dbg (Docker-Preload Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client $docker_ld_preload_lib + write_script_header $cmd3_file $tmp_gdb_cmdfile "$title3" "sleep 3" + echo "$get_docker_server_ip4addr" >> $cmd3_file + echo "docker run -it --cpuset-cpus='4-7' -v $vpp_shm_dir:$vpp_shm_dir -v $vpp_dir:$docker_vpp_dir -v $lib64_dir:$docker_lib64_dir -v $ld_preload_dir:$docker_ld_preload_dir -v $vppcom_conf_dir:$docker_vppcom_conf_dir -e VPPCOM_CONF=${docker_vppcom_conf_dir}$vppcom_conf -e LD_LIBRARY_PATH=$docker_lib64_dir ${docker_ld_preload}$docker_os ${docker_app_dir}${clnt_app}" >> $cmd3_file + write_script_footer $cmd3_file $perf_client + chmod +x $cmd3_file + fi +} + +docker_vcl() { + verify_no_vpp + verify_no_docker_containers + banner="Running DOCKER-VCL socket test" + + title1="VPP$title_dbg (Docker-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_vpp + gdb_cmdfile=$VPP_GDB_CMDFILE + set_pre_cmd $emacs_vpp $gdb_vpp + write_script_header $cmd1_file $tmp_gdb_cmdfile "$title1" + if [ -n "$multi_host" ] ; then + echo -n "sudo " >> $cmd1_file + fi + echo "${pre_cmd}$vpp_dir$vpp_app $vpp_args" >> $cmd1_file + write_script_footer $cmd1_file $perf_vpp + chmod +x $cmd1_file + + if [ -z "$multi_host" ] || [ "$multi_host" = "server" ] ; then + title2="SERVER$title_dbg (Docker-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_server + gdb_cmdfile=$VPPCOM_SERVER_GDB_CMDFILE + set_pre_cmd $emacs_server $gdb_server + write_script_header $cmd2_file $tmp_gdb_cmdfile "$title2" "sleep 2" + echo "docker run -it --cpuset-cpus='4-7' -v $vpp_shm_dir:$vpp_shm_dir -v $vpp_dir:$docker_vpp_dir -v $lib64_dir:$docker_lib64_dir -v $vppcom_conf_dir:$docker_vppcom_conf_dir -p $sock_srvr_port:$sock_srvr_port -e VPPCOM_CONF=${docker_vppcom_conf_dir}/$vppcom_conf -e LD_LIBRARY_PATH=$docker_lib64_dir $docker_os ${docker_app_dir}${srvr_app}" >> $cmd2_file + write_script_footer $cmd2_file $perf_server + chmod +x $cmd2_file + fi + + if [ -z "$multi_host" ] || [ "$multi_host" = "client" ] ; then + title3="CLIENT$title_dbg (Docker-VCL Socket Test)" + tmp_gdb_cmdfile=$tmp_gdb_cmdfile_client + gdb_cmdfile=$VPPCOM_CLIENT_GDB_CMDFILE + set_pre_cmd $emacs_client $gdb_client + write_script_header $cmd3_file $tmp_gdb_cmdfile "$title3" "sleep 3" + echo "$get_docker_server_ip4addr" >> $cmd3_file + echo "docker run -it --cpuset-cpus='4-7' -v $vpp_shm_dir:$vpp_shm_dir -v $vpp_dir:$docker_vpp_dir -v $lib64_dir:$docker_lib64_dir -v $vppcom_conf_dir:$docker_vppcom_conf_dir -e VPPCOM_CONF=${docker_vppcom_conf_dir}/$vppcom_conf -e LD_LIBRARY_PATH=$docker_lib64_dir $docker_os ${docker_app_dir}${clnt_app}" >> $cmd3_file + write_script_footer $cmd3_file $perf_client + chmod +x $cmd3_file + fi +} + +if [[ $run_test ]] ; then + eval $run_test +else + echo "ERROR: Please specify a test to run!" >&2 + usage; +fi + +if (( $(which xfce4-terminal | wc -l) > 0 )) ; then + xterm_cmd="xfce4-terminal --geometry $xterm_geom" + if [[ $use_tabs ]] ; then + declare -a tab_cmd_files + declare -a tab_titles + declare -i i=0 + + if [ -x "$cmd1_file" ] ; then + tab_cmd_files[$i]="$cmd1_file" + tab_titles[$i]="$title1" + (( i++ )) + fi + if [ -x "$cmd2_file" ] ; then + tab_cmd_files[$i]="$cmd2_file" + tab_titles[$i]="$title2" + (( i++ )) + fi + if [ -x "$cmd3_file" ] ; then + tab_cmd_files[$i]="$cmd3_file" + tab_titles[$i]="$title3" + fi + + if [ -n "${tab_cmd_files[2]}" ] ; then + $xterm_cmd --title "${tab_titles[0]}" --command "${tab_cmd_files[0]}" --tab --title "${tab_titles[1]}" --command "${tab_cmd_files[1]}" --tab --title "${tab_titles[2]}" --command "${tab_cmd_files[2]}" + elif [ -n "${tab_cmd_files[1]}" ] ; then + $xterm_cmd --title "${tab_titles[0]}" --command "${tab_cmd_files[0]}" --tab --title "${tab_titles[1]}" --command "${tab_cmd_files[1]}" + + else + $xterm_cmd --title "${tab_titles[0]}" --command "${tab_cmd_files[0]}" + fi + + else + if [ -x "$cmd1_file" ] ; then + ($xterm_cmd --title "$title1" --command "$cmd1_file" &) + fi + if [ -x "$cmd2_file" ] ; then + ($xterm_cmd --title "$title2" --command "$cmd2_file" &) + fi + if [ -x "$cmd3_file" ] ; then + ($xterm_cmd --title "$title3" --command "$cmd3_file" &) + fi + fi + +else + if [[ $use_tabs ]] ; then + echo "Sorry, plain ol' xterm doesn't support tabs." + fi + xterm_cmd="xterm -fs 10 -geometry $xterm_geom" + if [ -x "$cmd1_file" ] ; then + ($xterm_cmd -title "$title1" -e "$cmd1_file" &) + fi + if [ -x "$cmd2_file" ] ; then + ($xterm_cmd -title "$title2" -e "$cmd2_file" &) + fi + if [ -x "$cmd3_file" ] ; then + ($xterm_cmd -title "$title3" -e "$cmd3_file" &) + fi +fi + +sleep 1 diff --git a/test/scripts/test-loop.sh b/test/scripts/test-loop.sh new file mode 100755 index 00000000..51f5d5ce --- /dev/null +++ b/test/scripts/test-loop.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +function usage() { + echo "$0" 1>&2 + echo "" 1>&2 + echo "Usage: $0 [-p <pre-exec-cmd>] [-m <email>] -- <make test options|verify>" 1>&2 + echo "" 1>&2 + echo "Parameters:" 1>&2 + echo " -p <pre-exec-cmd> - run a command before each test loop (e.g. 'git pull')" 1>&2 + echo " -m <email> - if set, email is sent to this address on failure" 1>&2 + echo "" 1>&2 + echo "Examples:" 1>&2 + echo " $0 -m <somebody@cisco.com> -- test-debug TEST=l2bd" 1>&2 + echo " $0 -m <somebody@cisco.com> -- verify" 1>&2 + exit 1; +} + +PRE_EXEC_CMD="" +EMAIL="" + +while getopts "p:m:h" o; do + case "${o}" in + p) + PRE_EXEC_CMD=${OPTARG} + ;; + m) + regex="^[a-z0-9!#\$%&'*+/=?^_\`{|}~-]+(\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]*[a-z0-9])?\$" + m=${OPTARG} + if [[ ! $m =~ $regex ]] + then + echo "Invalid -m parameter value: \`$m'" >&2 + usage + fi + EMAIL="$m" + ;; + h) + usage + ;; + ?) + usage + ;; +esac + done +shift $((OPTIND-1)) + +if ! echo $* | grep test >/dev/null +then + if ! echo $* | grep verify >/dev/null + then + echo "Error: command line doesn't look right - should contain \`test' or \`verify' token..." >&2 + usage + fi +fi + +function finish { + NOW=`date +%s` + RUNTIME=$((NOW - START)) + AVG=$(echo "scale=2; $RUNTIME/$COUNT" | bc) + OUT="*********************************************************************" + OUT="$OUT\n* tail -n 30 $TMP:" + OUT="$OUT\n*********************************************************************" + OUT="$OUT\n`tail -n 30 $TMP`" + OUT="$OUT\n*********************************************************************" + OUT="$OUT\n* Total runtime: ${RUNTIME}s" + OUT="$OUT\n* Iterations: ${COUNT}" + OUT="$OUT\n* Average time: ${AVG}s" + OUT="$OUT\n* Log file: ${TMP}" + OUT="$OUT\n*********************************************************************" + echo -e "$OUT" + if [[ "$EMAIL" != "" && "$REASON" != "" ]] + then + SUBJECT="test loop finished ($REASON)" + echo -e "$OUT" | mail -s "$SUBJECT" $EMAIL + fi +} + +trap "echo Caught signal, exiting...; REASON=\"received signal\"; finish; exit -1" SIGINT SIGTERM + +TMP=`mktemp` +START=`date +%s` +COUNT=0 + +if ! test -f "$TMP" +then + echo "Couldn't create temporary file!" + exit -1 +fi + +echo "Temporary file is $TMP" +CMD="make $*" +echo "Command line is \`$CMD'" + +REASON="" +while true +do + COUNT=$((COUNT+1)) + BEFORE=`date +%s` + if [[ "$PRE_EXEC_CMD" != "" ]] + then + echo "Executing \`$PRE_EXEC_CMD' before test.." + if ! ($PRE_EXEC_CMD 2>&1 | tee $TMP) + then + echo "\`$PRE_EXEC_CMD' failed!" >&2 + REASON="$PRE_EXEC_CMD failed" + break + fi + fi + echo -n "Running test iteration #$COUNT..." + if ! ($CMD >$TMP 2>&1) + then + AFTER=`date +%s` + RUNTIME=$((AFTER-BEFORE)) + echo "FAILED! (after ${RUNTIME}s)" + REASON="test failed" + break + fi + AFTER=`date +%s` + RUNTIME=$((AFTER-BEFORE)) + echo "PASSED (after ${RUNTIME}s)" +done + +finish +exit 1 diff --git a/test/template_bd.py b/test/template_bd.py new file mode 100644 index 00000000..080b2e6b --- /dev/null +++ b/test/template_bd.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +from abc import abstractmethod, ABCMeta + +from scapy.layers.l2 import Ether, Raw +from scapy.layers.inet import IP, UDP + +from util import ip4_range + + +class BridgeDomain(object): + """ Bridge domain abstraction """ + __metaclass__ = ABCMeta + + @property + def frame_request(self): + """ Ethernet frame modeling a generic request """ + return (Ether(src='00:00:00:00:00:01', dst='00:00:00:00:00:02') / + IP(src='1.2.3.4', dst='4.3.2.1') / + UDP(sport=10000, dport=20000) / + Raw('\xa5' * 100)) + + @property + def frame_reply(self): + """ Ethernet frame modeling a generic reply """ + return (Ether(src='00:00:00:00:00:02', dst='00:00:00:00:00:01') / + IP(src='4.3.2.1', dst='1.2.3.4') / + UDP(sport=20000, dport=10000) / + Raw('\xa5' * 100)) + + @abstractmethod + def encap_mcast(self, pkt, src_ip, src_mac, vni): + """ Encapsulate mcast packet """ + pass + + @abstractmethod + def encapsulate(self, pkt, vni): + """ Encapsulate packet """ + pass + + @abstractmethod + def decapsulate(self, pkt): + """ Decapsulate packet """ + pass + + @abstractmethod + def check_encapsulation(self, pkt, vni, local_only=False): + """ Verify the encapsulation """ + pass + + def assert_eq_pkts(self, pkt1, pkt2): + """ Verify the Ether, IP, UDP, payload are equal in both + packets + """ + self.assertEqual(pkt1[Ether].src, pkt2[Ether].src) + self.assertEqual(pkt1[Ether].dst, pkt2[Ether].dst) + self.assertEqual(pkt1[IP].src, pkt2[IP].src) + self.assertEqual(pkt1[IP].dst, pkt2[IP].dst) + self.assertEqual(pkt1[UDP].sport, pkt2[UDP].sport) + self.assertEqual(pkt1[UDP].dport, pkt2[UDP].dport) + self.assertEqual(pkt1[Raw], pkt2[Raw]) + + def test_decap(self): + """ Decapsulation test + Send encapsulated frames from pg0 + Verify receipt of decapsulated frames on pg1 + """ + + encapsulated_pkt = self.encapsulate(self.frame_request, + self.single_tunnel_bd) + + self.pg0.add_stream([encapsulated_pkt, ]) + + self.pg1.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's the non-encapsulated + # frame + out = self.pg1.get_capture(1) + pkt = out[0] + self.assert_eq_pkts(pkt, self.frame_request) + + def test_encap(self): + """ Encapsulation test + Send frames from pg1 + Verify receipt of encapsulated frames on pg0 + """ + self.pg1.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.single_tunnel_bd) + + payload = self.decapsulate(pkt) + self.assert_eq_pkts(payload, self.frame_reply) + + def test_ucast_flood(self): + """ Unicast flood test + Send frames from pg3 + Verify receipt of encapsulated frames on pg0 + """ + self.pg3.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Get packet from each tunnel and assert it's corectly encapsulated. + out = self.pg0.get_capture(self.n_ucast_tunnels) + for pkt in out: + self.check_encapsulation(pkt, self.ucast_flood_bd, True) + payload = self.decapsulate(pkt) + self.assert_eq_pkts(payload, self.frame_reply) + + def test_mcast_flood(self): + """ Multicast flood test + Send frames from pg2 + Verify receipt of encapsulated frames on pg0 + """ + self.pg2.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.mcast_flood_bd, + local_only=False, mcast_pkt=True) + + payload = self.decapsulate(pkt) + self.assert_eq_pkts(payload, self.frame_reply) + + def test_mcast_rcv(self): + """ Multicast receive test + Send 20 encapsulated frames from pg0 only 10 match unicast tunnels + Verify receipt of 10 decap frames on pg2 + """ + mac = self.pg0.remote_mac + ip_range_start = 10 + ip_range_end = 30 + mcast_stream = [ + self.encap_mcast(self.frame_request, ip, mac, self.mcast_flood_bd) + for ip in ip4_range(self.pg0.remote_ip4, + ip_range_start, ip_range_end)] + self.pg0.add_stream(mcast_stream) + self.pg2.enable_capture() + self.pg_start() + out = self.pg2.get_capture(10) + for pkt in out: + self.assert_eq_pkts(pkt, self.frame_request) diff --git a/test/test_acl_plugin.py b/test/test_acl_plugin.py new file mode 100644 index 00000000..cd375a2c --- /dev/null +++ b/test/test_acl_plugin.py @@ -0,0 +1,1313 @@ +#!/usr/bin/env python +"""ACL plugin Test Case HLD: +""" + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, TCP, UDP, ICMP +from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest +from scapy.layers.inet6 import IPv6ExtHdrFragment +from framework import VppTestCase, VppTestRunner +from util import Host, ppp + + +class TestACLplugin(VppTestCase): + """ ACL plugin Test Case """ + + # traffic types + IP = 0 + ICMP = 1 + + # IP version + IPRANDOM = -1 + IPV4 = 0 + IPV6 = 1 + + # rule types + DENY = 0 + PERMIT = 1 + + # supported protocols + proto = [[6, 17], [1, 58]] + proto_map = {1: 'ICMP', 58: 'ICMPv6EchoRequest', 6: 'TCP', 17: 'UDP'} + ICMPv4 = 0 + ICMPv6 = 1 + TCP = 0 + UDP = 1 + PROTO_ALL = 0 + + # port ranges + PORTS_ALL = -1 + PORTS_RANGE = 0 + PORTS_RANGE_2 = 1 + udp_sport_from = 10 + udp_sport_to = udp_sport_from + 5 + udp_dport_from = 20000 + udp_dport_to = udp_dport_from + 5000 + tcp_sport_from = 30 + tcp_sport_to = tcp_sport_from + 5 + tcp_dport_from = 40000 + tcp_dport_to = tcp_dport_from + 5000 + + udp_sport_from_2 = 90 + udp_sport_to_2 = udp_sport_from_2 + 5 + udp_dport_from_2 = 30000 + udp_dport_to_2 = udp_dport_from_2 + 5000 + tcp_sport_from_2 = 130 + tcp_sport_to_2 = tcp_sport_from_2 + 5 + tcp_dport_from_2 = 20000 + tcp_dport_to_2 = tcp_dport_from_2 + 5000 + + icmp4_type = 8 # echo request + icmp4_code = 3 + icmp6_type = 128 # echo request + icmp6_code = 3 + + icmp4_type_2 = 8 + icmp4_code_from_2 = 5 + icmp4_code_to_2 = 20 + icmp6_type_2 = 128 + icmp6_code_from_2 = 8 + icmp6_code_to_2 = 42 + + # Test variables + bd_id = 1 + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestACLplugin, cls).setUpClass() + + random.seed() + + try: + # Create 2 pg interfaces + cls.create_pg_interfaces(range(2)) + + # Packet flows mapping pg0 -> pg1, pg2 etc. + cls.flows = dict() + cls.flows[cls.pg0] = [cls.pg1] + + # Packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + # Create BD with MAC learning and unknown unicast flooding disabled + # and put interfaces to this BD + cls.vapi.bridge_domain_add_del(bd_id=cls.bd_id, uu_flood=1, + learn=1) + for pg_if in cls.pg_interfaces: + cls.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, + bd_id=cls.bd_id) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + # Mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + for pg_if in cls.pg_interfaces: + cls.hosts_by_pg_idx[pg_if.sw_if_index] = [] + + # Create list of deleted hosts + cls.deleted_hosts_by_pg_idx = dict() + for pg_if in cls.pg_interfaces: + cls.deleted_hosts_by_pg_idx[pg_if.sw_if_index] = [] + + # warm-up the mac address tables + # self.warmup_test() + + except Exception: + super(TestACLplugin, cls).tearDownClass() + raise + + def setUp(self): + super(TestACLplugin, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestACLplugin, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show acl-plugin acl")) + self.logger.info(self.vapi.ppcli("show acl-plugin interface")) + self.logger.info(self.vapi.ppcli("show acl-plugin tables")) + self.logger.info(self.vapi.ppcli("show bridge-domain %s detail" + % self.bd_id)) + + def create_hosts(self, count, start=0): + """ + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address. + + :param int count: Number of hosts to create MAC/IPv4 addresses for. + :param int start: Number to start numbering from. + """ + n_int = len(self.pg_interfaces) + macs_per_if = count / n_int + i = -1 + for pg_if in self.pg_interfaces: + i += 1 + start_nr = macs_per_if * i + start + end_nr = count + start if i == (n_int - 1) \ + else macs_per_if * (i + 1) + start + hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] + for j in range(start_nr, end_nr): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j), + "2017:dead:%02x::%u" % (pg_if.sw_if_index, j)) + hosts.append(host) + + def create_rule(self, ip=0, permit_deny=0, ports=PORTS_ALL, proto=-1, + s_prefix=0, s_ip='\x00\x00\x00\x00', + d_prefix=0, d_ip='\x00\x00\x00\x00'): + if proto == -1: + return + if ports == self.PORTS_ALL: + sport_from = 0 + dport_from = 0 + sport_to = 65535 if proto != 1 and proto != 58 else 255 + dport_to = sport_to + elif ports == self.PORTS_RANGE: + if proto == 1: + sport_from = self.icmp4_type + sport_to = self.icmp4_type + dport_from = self.icmp4_code + dport_to = self.icmp4_code + elif proto == 58: + sport_from = self.icmp6_type + sport_to = self.icmp6_type + dport_from = self.icmp6_code + dport_to = self.icmp6_code + elif proto == self.proto[self.IP][self.TCP]: + sport_from = self.tcp_sport_from + sport_to = self.tcp_sport_to + dport_from = self.tcp_dport_from + dport_to = self.tcp_dport_to + elif proto == self.proto[self.IP][self.UDP]: + sport_from = self.udp_sport_from + sport_to = self.udp_sport_to + dport_from = self.udp_dport_from + dport_to = self.udp_dport_to + elif ports == self.PORTS_RANGE_2: + if proto == 1: + sport_from = self.icmp4_type_2 + sport_to = self.icmp4_type_2 + dport_from = self.icmp4_code_from_2 + dport_to = self.icmp4_code_to_2 + elif proto == 58: + sport_from = self.icmp6_type_2 + sport_to = self.icmp6_type_2 + dport_from = self.icmp6_code_from_2 + dport_to = self.icmp6_code_to_2 + elif proto == self.proto[self.IP][self.TCP]: + sport_from = self.tcp_sport_from_2 + sport_to = self.tcp_sport_to_2 + dport_from = self.tcp_dport_from_2 + dport_to = self.tcp_dport_to_2 + elif proto == self.proto[self.IP][self.UDP]: + sport_from = self.udp_sport_from_2 + sport_to = self.udp_sport_to_2 + dport_from = self.udp_dport_from_2 + dport_to = self.udp_dport_to_2 + else: + sport_from = ports + sport_to = ports + dport_from = ports + dport_to = ports + + rule = ({'is_permit': permit_deny, 'is_ipv6': ip, 'proto': proto, + 'srcport_or_icmptype_first': sport_from, + 'srcport_or_icmptype_last': sport_to, + 'src_ip_prefix_len': s_prefix, + 'src_ip_addr': s_ip, + 'dstport_or_icmpcode_first': dport_from, + 'dstport_or_icmpcode_last': dport_to, + 'dst_ip_prefix_len': d_prefix, + 'dst_ip_addr': d_ip}) + return rule + + def apply_rules(self, rules, tag=''): + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=rules, + tag=tag) + self.logger.info("Dumped ACL: " + str( + self.vapi.acl_dump(reply.acl_index))) + # Apply a ACL on the interface as inbound + for i in self.pg_interfaces: + self.vapi.acl_interface_set_acl_list(sw_if_index=i.sw_if_index, + n_input=1, + acls=[reply.acl_index]) + return + + def create_upper_layer(self, packet_index, proto, ports=0): + p = self.proto_map[proto] + if p == 'UDP': + if ports == 0: + return UDP(sport=random.randint(self.udp_sport_from, + self.udp_sport_to), + dport=random.randint(self.udp_dport_from, + self.udp_dport_to)) + else: + return UDP(sport=ports, dport=ports) + elif p == 'TCP': + if ports == 0: + return TCP(sport=random.randint(self.tcp_sport_from, + self.tcp_sport_to), + dport=random.randint(self.tcp_dport_from, + self.tcp_dport_to)) + else: + return TCP(sport=ports, dport=ports) + return '' + + def create_stream(self, src_if, packet_sizes, traffic_type=0, ipv6=0, + proto=-1, ports=0, fragments=False, pkt_raw=True): + """ + Create input packet stream for defined interface using hosts or + deleted_hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :param traffic_type: 1: ICMP packet, 2: IPv6 with EH, 0: otherwise. + :return: Stream of packets. + """ + pkts = [] + if self.flows.__contains__(src_if): + src_hosts = self.hosts_by_pg_idx[src_if.sw_if_index] + for dst_if in self.flows[src_if]: + dst_hosts = self.hosts_by_pg_idx[dst_if.sw_if_index] + n_int = len(dst_hosts) * len(src_hosts) + for i in range(0, n_int): + dst_host = dst_hosts[i / len(src_hosts)] + src_host = src_hosts[i % len(src_hosts)] + pkt_info = self.create_packet_info(src_if, dst_if) + if ipv6 == 1: + pkt_info.ip = 1 + elif ipv6 == 0: + pkt_info.ip = 0 + else: + pkt_info.ip = random.choice([0, 1]) + if proto == -1: + pkt_info.proto = random.choice(self.proto[self.IP]) + else: + pkt_info.proto = proto + payload = self.info_to_payload(pkt_info) + p = Ether(dst=dst_host.mac, src=src_host.mac) + if pkt_info.ip: + p /= IPv6(dst=dst_host.ip6, src=src_host.ip6) + if fragments: + p /= IPv6ExtHdrFragment(offset=64, m=1) + else: + if fragments: + p /= IP(src=src_host.ip4, dst=dst_host.ip4, + flags=1, frag=64) + else: + p /= IP(src=src_host.ip4, dst=dst_host.ip4) + if traffic_type == self.ICMP: + if pkt_info.ip: + p /= ICMPv6EchoRequest(type=self.icmp6_type, + code=self.icmp6_code) + else: + p /= ICMP(type=self.icmp4_type, + code=self.icmp4_code) + else: + p /= self.create_upper_layer(i, pkt_info.proto, ports) + if pkt_raw: + p /= Raw(payload) + pkt_info.data = p.copy() + if pkt_raw: + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, pg_if, capture, traffic_type=0, ip_type=0): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + :param traffic_type: 1: ICMP packet, 2: IPv6 with EH, 0: otherwise. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + try: + # Raw data for ICMPv6 are stored in ICMPv6EchoRequest.data + if traffic_type == self.ICMP and ip_type == self.IPV6: + payload_info = self.payload_to_info( + packet[ICMPv6EchoRequest].data) + payload = packet[ICMPv6EchoRequest] + else: + payload_info = self.payload_to_info(str(packet[Raw])) + payload = packet[self.proto_map[payload_info.proto]] + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + + if ip_type != 0: + self.assertEqual(payload_info.ip, ip_type) + if traffic_type == self.ICMP: + try: + if payload_info.ip == 0: + self.assertEqual(payload.type, self.icmp4_type) + self.assertEqual(payload.code, self.icmp4_code) + else: + self.assertEqual(payload.type, self.icmp6_type) + self.assertEqual(payload.code, self.icmp6_code) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + else: + try: + ip_version = IPv6 if payload_info.ip == 1 else IP + + ip = packet[ip_version] + packet_index = payload_info.index + + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, + packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[ip_version].src) + self.assertEqual(ip.dst, saved_packet[ip_version].dst) + p = self.proto_map[payload_info.proto] + if p == 'TCP': + tcp = packet[TCP] + self.assertEqual(tcp.sport, saved_packet[ + TCP].sport) + self.assertEqual(tcp.dport, saved_packet[ + TCP].dport) + elif p == 'UDP': + udp = packet[UDP] + self.assertEqual(udp.sport, saved_packet[ + UDP].sport) + self.assertEqual(udp.dport, saved_packet[ + UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", + packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def run_traffic_no_check(self): + # Test + # Create incoming packet streams for packet-generator interfaces + for i in self.pg_interfaces: + if self.flows.__contains__(i): + pkts = self.create_stream(i, self.pg_if_packet_sizes) + if len(pkts) > 0: + i.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + def run_verify_test(self, traffic_type=0, ip_type=0, proto=-1, ports=0, + frags=False, pkt_raw=True): + # Test + # Create incoming packet streams for packet-generator interfaces + pkts_cnt = 0 + for i in self.pg_interfaces: + if self.flows.__contains__(i): + pkts = self.create_stream(i, self.pg_if_packet_sizes, + traffic_type, ip_type, proto, ports, + frags, pkt_raw) + if len(pkts) > 0: + i.add_stream(pkts) + pkts_cnt += len(pkts) + + # Enable packet capture and start packet sendingself.IPV + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for src_if in self.pg_interfaces: + if self.flows.__contains__(src_if): + for dst_if in self.flows[src_if]: + capture = dst_if.get_capture(pkts_cnt) + self.logger.info("Verifying capture on interface %s" % + dst_if.name) + self.verify_capture(dst_if, capture, traffic_type, ip_type) + + def run_verify_negat_test(self, traffic_type=0, ip_type=0, proto=-1, + ports=0, frags=False): + # Test + self.reset_packet_infos() + for i in self.pg_interfaces: + if self.flows.__contains__(i): + pkts = self.create_stream(i, self.pg_if_packet_sizes, + traffic_type, ip_type, proto, ports, + frags) + if len(pkts) > 0: + i.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for src_if in self.pg_interfaces: + if self.flows.__contains__(src_if): + for dst_if in self.flows[src_if]: + self.logger.info("Verifying capture on interface %s" % + dst_if.name) + capture = dst_if.get_capture(0) + self.assertEqual(len(capture), 0) + + def test_0000_warmup_test(self): + """ ACL plugin version check; learn MACs + """ + self.create_hosts(16) + self.run_traffic_no_check() + reply = self.vapi.papi.acl_plugin_get_version() + self.assertEqual(reply.major, 1) + self.logger.info("Working with ACL plugin version: %d.%d" % ( + reply.major, reply.minor)) + # minor version changes are non breaking + # self.assertEqual(reply.minor, 0) + + def test_0001_acl_create(self): + """ ACL create/delete test + """ + + self.logger.info("ACLP_TEST_START_0001") + # Add an ACL + r = [{'is_permit': 1, 'is_ipv6': 0, 'proto': 17, + 'srcport_or_icmptype_first': 1234, + 'srcport_or_icmptype_last': 1235, + 'src_ip_prefix_len': 0, + 'src_ip_addr': '\x00\x00\x00\x00', + 'dstport_or_icmpcode_first': 1234, + 'dstport_or_icmpcode_last': 1234, + 'dst_ip_addr': '\x00\x00\x00\x00', + 'dst_ip_prefix_len': 0}] + # Test 1: add a new ACL + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=r, + tag="permit 1234") + self.assertEqual(reply.retval, 0) + # The very first ACL gets #0 + self.assertEqual(reply.acl_index, 0) + first_acl = reply.acl_index + rr = self.vapi.acl_dump(reply.acl_index) + self.logger.info("Dumped ACL: " + str(rr)) + self.assertEqual(len(rr), 1) + # We should have the same number of ACL entries as we had asked + self.assertEqual(len(rr[0].r), len(r)) + # The rules should be the same. But because the submitted and returned + # are different types, we need to iterate over rules and keys to get + # to basic values. + for i_rule in range(0, len(r) - 1): + for rule_key in r[i_rule]: + self.assertEqual(rr[0].r[i_rule][rule_key], + r[i_rule][rule_key]) + + # Add a deny-1234 ACL + r_deny = [{'is_permit': 0, 'is_ipv6': 0, 'proto': 17, + 'srcport_or_icmptype_first': 1234, + 'srcport_or_icmptype_last': 1235, + 'src_ip_prefix_len': 0, + 'src_ip_addr': '\x00\x00\x00\x00', + 'dstport_or_icmpcode_first': 1234, + 'dstport_or_icmpcode_last': 1234, + 'dst_ip_addr': '\x00\x00\x00\x00', + 'dst_ip_prefix_len': 0}, + {'is_permit': 1, 'is_ipv6': 0, 'proto': 17, + 'srcport_or_icmptype_first': 0, + 'srcport_or_icmptype_last': 0, + 'src_ip_prefix_len': 0, + 'src_ip_addr': '\x00\x00\x00\x00', + 'dstport_or_icmpcode_first': 0, + 'dstport_or_icmpcode_last': 0, + 'dst_ip_addr': '\x00\x00\x00\x00', + 'dst_ip_prefix_len': 0}] + + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=r_deny, + tag="deny 1234;permit all") + self.assertEqual(reply.retval, 0) + # The second ACL gets #1 + self.assertEqual(reply.acl_index, 1) + second_acl = reply.acl_index + + # Test 2: try to modify a nonexistent ACL + reply = self.vapi.acl_add_replace(acl_index=432, r=r, + tag="FFFF:FFFF", expected_retval=-1) + self.assertEqual(reply.retval, -1) + # The ACL number should pass through + self.assertEqual(reply.acl_index, 432) + # apply an ACL on an interface inbound, try to delete ACL, must fail + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=1, + acls=[first_acl]) + reply = self.vapi.acl_del(acl_index=first_acl, expected_retval=-1) + # Unapply an ACL and then try to delete it - must be ok + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=0, + acls=[]) + reply = self.vapi.acl_del(acl_index=first_acl, expected_retval=0) + + # apply an ACL on an interface outbound, try to delete ACL, must fail + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=0, + acls=[second_acl]) + reply = self.vapi.acl_del(acl_index=second_acl, expected_retval=-1) + # Unapply the ACL and then try to delete it - must be ok + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=0, + acls=[]) + reply = self.vapi.acl_del(acl_index=second_acl, expected_retval=0) + + # try to apply a nonexistent ACL - must fail + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=1, + acls=[first_acl], + expected_retval=-1) + + self.logger.info("ACLP_TEST_FINISH_0001") + + def test_0002_acl_permit_apply(self): + """ permit ACL apply test + """ + self.logger.info("ACLP_TEST_START_0002") + + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, + 0, self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV4, self.PERMIT, + 0, self.proto[self.IP][self.TCP])) + + # Apply rules + self.apply_rules(rules, "permit per-flow") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, -1) + self.logger.info("ACLP_TEST_FINISH_0002") + + def test_0003_acl_deny_apply(self): + """ deny ACL apply test + """ + self.logger.info("ACLP_TEST_START_0003") + # Add a deny-flows ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, + self.PORTS_ALL, self.proto[self.IP][self.UDP])) + # Permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny per-flow;permit all") + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPV4, + self.proto[self.IP][self.UDP]) + self.logger.info("ACLP_TEST_FINISH_0003") + # self.assertEqual(1, 0) + + def test_0004_vpp624_permit_icmpv4(self): + """ VPP_624 permit ICMPv4 + """ + self.logger.info("ACLP_TEST_START_0004") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, self.PORTS_RANGE, + self.proto[self.ICMP][self.ICMPv4])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit icmpv4") + + # Traffic should still pass + self.run_verify_test(self.ICMP, self.IPV4, + self.proto[self.ICMP][self.ICMPv4]) + + self.logger.info("ACLP_TEST_FINISH_0004") + + def test_0005_vpp624_permit_icmpv6(self): + """ VPP_624 permit ICMPv6 + """ + self.logger.info("ACLP_TEST_START_0005") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, self.PORTS_RANGE, + self.proto[self.ICMP][self.ICMPv6])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit icmpv6") + + # Traffic should still pass + self.run_verify_test(self.ICMP, self.IPV6, + self.proto[self.ICMP][self.ICMPv6]) + + self.logger.info("ACLP_TEST_FINISH_0005") + + def test_0006_vpp624_deny_icmpv4(self): + """ VPP_624 deny ICMPv4 + """ + self.logger.info("ACLP_TEST_START_0006") + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE, + self.proto[self.ICMP][self.ICMPv4])) + # permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny icmpv4") + + # Traffic should not pass + self.run_verify_negat_test(self.ICMP, self.IPV4, 0) + + self.logger.info("ACLP_TEST_FINISH_0006") + + def test_0007_vpp624_deny_icmpv6(self): + """ VPP_624 deny ICMPv6 + """ + self.logger.info("ACLP_TEST_START_0007") + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE, + self.proto[self.ICMP][self.ICMPv6])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny icmpv6") + + # Traffic should not pass + self.run_verify_negat_test(self.ICMP, self.IPV6, 0) + + self.logger.info("ACLP_TEST_FINISH_0007") + + def test_0008_tcp_permit_v4(self): + """ permit TCPv4 + """ + self.logger.info("ACLP_TEST_START_0008") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ipv4 tcp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0008") + + def test_0009_tcp_permit_v6(self): + """ permit TCPv6 + """ + self.logger.info("ACLP_TEST_START_0009") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip6 tcp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0008") + + def test_0010_udp_permit_v4(self): + """ permit UDPv4 + """ + self.logger.info("ACLP_TEST_START_0010") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ipv udp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0010") + + def test_0011_udp_permit_v6(self): + """ permit UDPv6 + """ + self.logger.info("ACLP_TEST_START_0011") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip6 udp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0011") + + def test_0012_tcp_deny(self): + """ deny TCPv4/v6 + """ + self.logger.info("ACLP_TEST_START_0012") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 tcp") + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0012") + + def test_0013_udp_deny(self): + """ deny UDPv4/v6 + """ + self.logger.info("ACLP_TEST_START_0013") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 udp") + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0013") + + def test_0014_acl_dump(self): + """ verify add/dump acls + """ + self.logger.info("ACLP_TEST_START_0014") + + r = [[self.IPV4, self.PERMIT, 1234, self.proto[self.IP][self.TCP]], + [self.IPV4, self.PERMIT, 2345, self.proto[self.IP][self.UDP]], + [self.IPV4, self.PERMIT, 0, self.proto[self.IP][self.TCP]], + [self.IPV4, self.PERMIT, 0, self.proto[self.IP][self.UDP]], + [self.IPV4, self.PERMIT, 5, self.proto[self.ICMP][self.ICMPv4]], + [self.IPV6, self.PERMIT, 4321, self.proto[self.IP][self.TCP]], + [self.IPV6, self.PERMIT, 5432, self.proto[self.IP][self.UDP]], + [self.IPV6, self.PERMIT, 0, self.proto[self.IP][self.TCP]], + [self.IPV6, self.PERMIT, 0, self.proto[self.IP][self.UDP]], + [self.IPV6, self.PERMIT, 6, self.proto[self.ICMP][self.ICMPv6]], + [self.IPV4, self.DENY, self.PORTS_ALL, 0], + [self.IPV4, self.DENY, 1234, self.proto[self.IP][self.TCP]], + [self.IPV4, self.DENY, 2345, self.proto[self.IP][self.UDP]], + [self.IPV4, self.DENY, 5, self.proto[self.ICMP][self.ICMPv4]], + [self.IPV6, self.DENY, 4321, self.proto[self.IP][self.TCP]], + [self.IPV6, self.DENY, 5432, self.proto[self.IP][self.UDP]], + [self.IPV6, self.DENY, 6, self.proto[self.ICMP][self.ICMPv6]], + [self.IPV6, self.DENY, self.PORTS_ALL, 0] + ] + + # Add and verify new ACLs + rules = [] + for i in range(len(r)): + rules.append(self.create_rule(r[i][0], r[i][1], r[i][2], r[i][3])) + + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=rules) + result = self.vapi.acl_dump(reply.acl_index) + + i = 0 + for drules in result: + for dr in drules.r: + self.assertEqual(dr.is_ipv6, r[i][0]) + self.assertEqual(dr.is_permit, r[i][1]) + self.assertEqual(dr.proto, r[i][3]) + + if r[i][2] > 0: + self.assertEqual(dr.srcport_or_icmptype_first, r[i][2]) + else: + if r[i][2] < 0: + self.assertEqual(dr.srcport_or_icmptype_first, 0) + self.assertEqual(dr.srcport_or_icmptype_last, 65535) + else: + if dr.proto == self.proto[self.IP][self.TCP]: + self.assertGreater(dr.srcport_or_icmptype_first, + self.tcp_sport_from-1) + self.assertLess(dr.srcport_or_icmptype_first, + self.tcp_sport_to+1) + self.assertGreater(dr.dstport_or_icmpcode_last, + self.tcp_dport_from-1) + self.assertLess(dr.dstport_or_icmpcode_last, + self.tcp_dport_to+1) + elif dr.proto == self.proto[self.IP][self.UDP]: + self.assertGreater(dr.srcport_or_icmptype_first, + self.udp_sport_from-1) + self.assertLess(dr.srcport_or_icmptype_first, + self.udp_sport_to+1) + self.assertGreater(dr.dstport_or_icmpcode_last, + self.udp_dport_from-1) + self.assertLess(dr.dstport_or_icmpcode_last, + self.udp_dport_to+1) + i += 1 + + self.logger.info("ACLP_TEST_FINISH_0014") + + def test_0015_tcp_permit_port_v4(self): + """ permit single TCPv4 + """ + self.logger.info("ACLP_TEST_START_0015") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, port, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip4 tcp "+str(port)) + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, + self.proto[self.IP][self.TCP], port) + + self.logger.info("ACLP_TEST_FINISH_0015") + + def test_0016_udp_permit_port_v4(self): + """ permit single UDPv4 + """ + self.logger.info("ACLP_TEST_START_0016") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, port, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip4 tcp "+str(port)) + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, + self.proto[self.IP][self.UDP], port) + + self.logger.info("ACLP_TEST_FINISH_0016") + + def test_0017_tcp_permit_port_v6(self): + """ permit single TCPv6 + """ + self.logger.info("ACLP_TEST_START_0017") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, port, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip4 tcp "+str(port)) + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, + self.proto[self.IP][self.TCP], port) + + self.logger.info("ACLP_TEST_FINISH_0017") + + def test_0018_udp_permit_port_v6(self): + """ permit single UPPv6 + """ + self.logger.info("ACLP_TEST_START_0018") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, port, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip4 tcp "+str(port)) + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, + self.proto[self.IP][self.UDP], port) + + self.logger.info("ACLP_TEST_FINISH_0018") + + def test_0019_udp_deny_port(self): + """ deny single TCPv4/v6 + """ + self.logger.info("ACLP_TEST_START_0019") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, port, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV6, self.DENY, port, + self.proto[self.IP][self.TCP])) + # Permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 udp "+str(port)) + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.TCP], port) + + self.logger.info("ACLP_TEST_FINISH_0019") + + def test_0020_udp_deny_port(self): + """ deny single UDPv4/v6 + """ + self.logger.info("ACLP_TEST_START_0020") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, port, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.DENY, port, + self.proto[self.IP][self.UDP])) + # Permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 udp "+str(port)) + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.UDP], port) + + self.logger.info("ACLP_TEST_FINISH_0020") + + def test_0021_udp_deny_port_verify_fragment_deny(self): + """ deny single UDPv4/v6, permit ip any, verify non-initial fragment blocked + """ + self.logger.info("ACLP_TEST_START_0021") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, port, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.DENY, port, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 udp "+str(port)) + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.UDP], port, True) + + self.logger.info("ACLP_TEST_FINISH_0021") + + def test_0022_zero_length_udp_ipv4(self): + """ VPP-687 zero length udp ipv4 packet""" + self.logger.info("ACLP_TEST_START_0022") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, port, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append( + self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit empty udp ip4 " + str(port)) + + # Traffic should still pass + # Create incoming packet streams for packet-generator interfaces + pkts_cnt = 0 + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes, + self.IP, self.IPV4, + self.proto[self.IP][self.UDP], port, + False, False) + if len(pkts) > 0: + self.pg0.add_stream(pkts) + pkts_cnt += len(pkts) + + # Enable packet capture and start packet sendingself.IPV + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg1.get_capture(pkts_cnt) + + self.logger.info("ACLP_TEST_FINISH_0022") + + def test_0023_zero_length_udp_ipv6(self): + """ VPP-687 zero length udp ipv6 packet""" + self.logger.info("ACLP_TEST_START_0023") + + port = random.randint(0, 65535) + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.PERMIT, port, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit empty udp ip6 "+str(port)) + + # Traffic should still pass + # Create incoming packet streams for packet-generator interfaces + pkts_cnt = 0 + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes, + self.IP, self.IPV6, + self.proto[self.IP][self.UDP], port, + False, False) + if len(pkts) > 0: + self.pg0.add_stream(pkts) + pkts_cnt += len(pkts) + + # Enable packet capture and start packet sendingself.IPV + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify outgoing packet streams per packet-generator interface + self.pg1.get_capture(pkts_cnt) + + self.logger.info("ACLP_TEST_FINISH_0023") + + def test_0108_tcp_permit_v4(self): + """ permit TCPv4 + non-match range + """ + self.logger.info("ACLP_TEST_START_0108") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE_2, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV4, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ipv4 tcp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0108") + + def test_0109_tcp_permit_v6(self): + """ permit TCPv6 + non-match range + """ + self.logger.info("ACLP_TEST_START_0109") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE_2, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV6, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip6 tcp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0109") + + def test_0110_udp_permit_v4(self): + """ permit UDPv4 + non-match range + """ + self.logger.info("ACLP_TEST_START_0110") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE_2, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV4, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ipv4 udp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV4, self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0110") + + def test_0111_udp_permit_v6(self): + """ permit UDPv6 + non-match range + """ + self.logger.info("ACLP_TEST_START_0111") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE_2, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.PERMIT, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # deny ip any any in the end + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "permit ip6 udp") + + # Traffic should still pass + self.run_verify_test(self.IP, self.IPV6, self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0111") + + def test_0112_tcp_deny(self): + """ deny TCPv4/v6 + non-match range + """ + self.logger.info("ACLP_TEST_START_0112") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_RANGE_2, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_RANGE_2, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.TCP])) + # permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 tcp") + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.TCP]) + + self.logger.info("ACLP_TEST_FINISH_0112") + + def test_0113_udp_deny(self): + """ deny UDPv4/v6 + non-match range + """ + self.logger.info("ACLP_TEST_START_0113") + + # Add an ACL + rules = [] + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_RANGE_2, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_RANGE_2, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV4, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + rules.append(self.create_rule(self.IPV6, self.DENY, self.PORTS_RANGE, + self.proto[self.IP][self.UDP])) + # permit ip any any in the end + rules.append(self.create_rule(self.IPV4, self.PERMIT, + self.PORTS_ALL, 0)) + rules.append(self.create_rule(self.IPV6, self.PERMIT, + self.PORTS_ALL, 0)) + + # Apply rules + self.apply_rules(rules, "deny ip4/ip6 udp") + + # Traffic should not pass + self.run_verify_negat_test(self.IP, self.IPRANDOM, + self.proto[self.IP][self.UDP]) + + self.logger.info("ACLP_TEST_FINISH_0113") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_acl_plugin_conns.py b/test/test_acl_plugin_conns.py new file mode 100644 index 00000000..43e8b69f --- /dev/null +++ b/test/test_acl_plugin_conns.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python +""" ACL plugin extended stateful tests """ + +import unittest +from framework import VppTestCase, VppTestRunner, running_extended_tests +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP, TCP +from scapy.packet import Packet +from socket import inet_pton, AF_INET, AF_INET6 +from scapy.layers.inet6 import IPv6, ICMPv6Unknown, ICMPv6EchoRequest +from scapy.layers.inet6 import ICMPv6EchoReply, IPv6ExtHdrRouting +from scapy.layers.inet6 import IPv6ExtHdrFragment +from pprint import pprint +from random import randint +from util import L4_Conn + + +def to_acl_rule(self, is_permit, wildcard_sport=False): + p = self + rule_family = AF_INET6 if p.haslayer(IPv6) else AF_INET + rule_prefix_len = 128 if p.haslayer(IPv6) else 32 + rule_l3_layer = IPv6 if p.haslayer(IPv6) else IP + rule_l4_sport = p.sport + rule_l4_dport = p.dport + if p.haslayer(IPv6): + rule_l4_proto = p[IPv6].nh + else: + rule_l4_proto = p[IP].proto + + if wildcard_sport: + rule_l4_sport_first = 0 + rule_l4_sport_last = 65535 + else: + rule_l4_sport_first = rule_l4_sport + rule_l4_sport_last = rule_l4_sport + + new_rule = { + 'is_permit': is_permit, + 'is_ipv6': p.haslayer(IPv6), + 'src_ip_addr': inet_pton(rule_family, + p[rule_l3_layer].src), + 'src_ip_prefix_len': rule_prefix_len, + 'dst_ip_addr': inet_pton(rule_family, + p[rule_l3_layer].dst), + 'dst_ip_prefix_len': rule_prefix_len, + 'srcport_or_icmptype_first': rule_l4_sport_first, + 'srcport_or_icmptype_last': rule_l4_sport_last, + 'dstport_or_icmpcode_first': rule_l4_dport, + 'dstport_or_icmpcode_last': rule_l4_dport, + 'proto': rule_l4_proto, + } + return new_rule + +Packet.to_acl_rule = to_acl_rule + + +class IterateWithSleep(): + def __init__(self, testcase, n_iters, description, sleep_sec): + self.curr = 0 + self.testcase = testcase + self.n_iters = n_iters + self.sleep_sec = sleep_sec + self.description = description + + def __iter__(self): + for x in range(0, self.n_iters): + yield x + self.testcase.sleep(self.sleep_sec) + + +class Conn(L4_Conn): + def apply_acls(self, reflect_side, acl_side): + pkts = [] + pkts.append(self.pkt(0)) + pkts.append(self.pkt(1)) + pkt = pkts[reflect_side] + + r = [] + r.append(pkt.to_acl_rule(2, wildcard_sport=True)) + r.append(self.wildcard_rule(0)) + res = self.testcase.vapi.acl_add_replace(0xffffffff, r) + self.testcase.assert_equal(res.retval, 0, "error adding ACL") + reflect_acl_index = res.acl_index + + r = [] + r.append(self.wildcard_rule(0)) + res = self.testcase.vapi.acl_add_replace(0xffffffff, r) + self.testcase.assert_equal(res.retval, 0, "error adding deny ACL") + deny_acl_index = res.acl_index + + if reflect_side == acl_side: + self.testcase.vapi.acl_interface_set_acl_list( + self.ifs[acl_side].sw_if_index, 1, + [reflect_acl_index, + deny_acl_index]) + self.testcase.vapi.acl_interface_set_acl_list( + self.ifs[1-acl_side].sw_if_index, 0, []) + else: + self.testcase.vapi.acl_interface_set_acl_list( + self.ifs[acl_side].sw_if_index, 1, + [deny_acl_index, + reflect_acl_index]) + self.testcase.vapi.acl_interface_set_acl_list( + self.ifs[1-acl_side].sw_if_index, 0, []) + + def wildcard_rule(self, is_permit): + any_addr = ["0.0.0.0", "::"] + rule_family = self.address_family + is_ip6 = 1 if rule_family == AF_INET6 else 0 + new_rule = { + 'is_permit': is_permit, + 'is_ipv6': is_ip6, + 'src_ip_addr': inet_pton(rule_family, any_addr[is_ip6]), + 'src_ip_prefix_len': 0, + 'dst_ip_addr': inet_pton(rule_family, any_addr[is_ip6]), + 'dst_ip_prefix_len': 0, + 'srcport_or_icmptype_first': 0, + 'srcport_or_icmptype_last': 65535, + 'dstport_or_icmpcode_first': 0, + 'dstport_or_icmpcode_last': 65535, + 'proto': 0, + } + return new_rule + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class ACLPluginConnTestCase(VppTestCase): + """ ACL plugin connection-oriented extended testcases """ + + @classmethod + def setUpClass(self): + super(ACLPluginConnTestCase, self).setUpClass() + # create pg0 and pg1 + self.create_pg_interfaces(range(2)) + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.config_ip6() + i.resolve_arp() + i.resolve_ndp() + + def tearDown(self): + """Run standard test teardown and log various show commands + """ + super(ACLPluginConnTestCase, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show ip arp")) + self.logger.info(self.vapi.cli("show ip6 neighbors")) + self.logger.info(self.vapi.cli("show acl-plugin sessions")) + self.logger.info(self.vapi.cli("show acl-plugin acl")) + self.logger.info(self.vapi.cli("show acl-plugin interface")) + self.logger.info(self.vapi.cli("show acl-plugin tables")) + + def run_basic_conn_test(self, af, acl_side): + """ Basic conn timeout test """ + conn1 = Conn(self, self.pg0, self.pg1, af, UDP, 42001, 4242) + conn1.apply_acls(0, acl_side) + conn1.send_through(0) + # the return packets should pass + conn1.send_through(1) + # send some packets on conn1, ensure it doesn't go away + for i in IterateWithSleep(self, 20, "Keep conn active", 0.3): + conn1.send_through(1) + # allow the conn to time out + for i in IterateWithSleep(self, 30, "Wait for timeout", 0.1): + pass + # now try to send a packet on the reflected side + try: + p2 = conn1.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + self.assert_equal(p2, None, "packet on long-idle conn") + + def run_active_conn_test(self, af, acl_side): + """ Idle connection behind active connection test """ + base = 10000 + 1000*acl_side + conn1 = Conn(self, self.pg0, self.pg1, af, UDP, base + 1, 2323) + conn2 = Conn(self, self.pg0, self.pg1, af, UDP, base + 2, 2323) + conn3 = Conn(self, self.pg0, self.pg1, af, UDP, base + 3, 2323) + conn1.apply_acls(0, acl_side) + conn1.send(0) + conn1.recv(1) + # create and check that the conn2/3 work + self.sleep(0.1) + conn2.send_pingpong(0) + self.sleep(0.1) + conn3.send_pingpong(0) + # send some packets on conn1, keep conn2/3 idle + for i in IterateWithSleep(self, 20, "Keep conn active", 0.2): + conn1.send_through(1) + try: + p2 = conn2.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + # We should have not received the packet on a long-idle + # connection, because it should have timed out + # If it didn't - it is a problem + self.assert_equal(p2, None, "packet on long-idle conn") + + def run_clear_conn_test(self, af, acl_side): + """ Clear the connections via CLI """ + conn1 = Conn(self, self.pg0, self.pg1, af, UDP, 42001, 4242) + conn1.apply_acls(0, acl_side) + conn1.send_through(0) + # the return packets should pass + conn1.send_through(1) + # send some packets on conn1, ensure it doesn't go away + for i in IterateWithSleep(self, 20, "Keep conn active", 0.3): + conn1.send_through(1) + # clear all connections + self.vapi.ppcli("clear acl-plugin sessions") + # now try to send a packet on the reflected side + try: + p2 = conn1.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + self.assert_equal(p2, None, "packet on supposedly deleted conn") + + def run_tcp_transient_setup_conn_test(self, af, acl_side): + conn1 = Conn(self, self.pg0, self.pg1, af, TCP, 53001, 5151) + conn1.apply_acls(0, acl_side) + conn1.send_through(0, 'S') + # the return packets should pass + conn1.send_through(1, 'SA') + # allow the conn to time out + for i in IterateWithSleep(self, 30, "Wait for timeout", 0.1): + pass + # ensure conn times out + try: + p2 = conn1.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + self.assert_equal(p2, None, "packet on supposedly deleted conn") + + def run_tcp_established_conn_test(self, af, acl_side): + conn1 = Conn(self, self.pg0, self.pg1, af, TCP, 53002, 5052) + conn1.apply_acls(0, acl_side) + conn1.send_through(0, 'S') + # the return packets should pass + conn1.send_through(1, 'SA') + # complete the threeway handshake + # (NB: sequence numbers not tracked, so not set!) + conn1.send_through(0, 'A') + # allow the conn to time out if it's in embryonic timer + for i in IterateWithSleep(self, 30, "Wait for transient timeout", 0.1): + pass + # Try to send the packet from the "forbidden" side - it must pass + conn1.send_through(1, 'A') + # ensure conn times out for real + for i in IterateWithSleep(self, 130, "Wait for timeout", 0.1): + pass + try: + p2 = conn1.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + self.assert_equal(p2, None, "packet on supposedly deleted conn") + + def run_tcp_transient_teardown_conn_test(self, af, acl_side): + conn1 = Conn(self, self.pg0, self.pg1, af, TCP, 53002, 5052) + conn1.apply_acls(0, acl_side) + conn1.send_through(0, 'S') + # the return packets should pass + conn1.send_through(1, 'SA') + # complete the threeway handshake + # (NB: sequence numbers not tracked, so not set!) + conn1.send_through(0, 'A') + # allow the conn to time out if it's in embryonic timer + for i in IterateWithSleep(self, 30, "Wait for transient timeout", 0.1): + pass + # Try to send the packet from the "forbidden" side - it must pass + conn1.send_through(1, 'A') + # Send the FIN to bounce the session out of established + conn1.send_through(1, 'FA') + # If conn landed on transient timer it will time out here + for i in IterateWithSleep(self, 30, "Wait for transient timeout", 0.1): + pass + # Now it should have timed out already + try: + p2 = conn1.send_through(1).command() + except: + # If we asserted while waiting, it's good. + # the conn should have timed out. + p2 = None + self.assert_equal(p2, None, "packet on supposedly deleted conn") + + def test_0000_conn_prepare_test(self): + """ Prepare the settings """ + self.vapi.ppcli("set acl-plugin session timeout udp idle 1") + + def test_0001_basic_conn_test(self): + """ IPv4: Basic conn timeout test reflect on ingress """ + self.run_basic_conn_test(AF_INET, 0) + + def test_0002_basic_conn_test(self): + """ IPv4: Basic conn timeout test reflect on egress """ + self.run_basic_conn_test(AF_INET, 1) + + def test_0005_clear_conn_test(self): + """ IPv4: reflect egress, clear conn """ + self.run_clear_conn_test(AF_INET, 1) + + def test_0006_clear_conn_test(self): + """ IPv4: reflect ingress, clear conn """ + self.run_clear_conn_test(AF_INET, 0) + + def test_0011_active_conn_test(self): + """ IPv4: Idle conn behind active conn, reflect on ingress """ + self.run_active_conn_test(AF_INET, 0) + + def test_0012_active_conn_test(self): + """ IPv4: Idle conn behind active conn, reflect on egress """ + self.run_active_conn_test(AF_INET, 1) + + def test_1001_basic_conn_test(self): + """ IPv6: Basic conn timeout test reflect on ingress """ + self.run_basic_conn_test(AF_INET6, 0) + + def test_1002_basic_conn_test(self): + """ IPv6: Basic conn timeout test reflect on egress """ + self.run_basic_conn_test(AF_INET6, 1) + + def test_1005_clear_conn_test(self): + """ IPv6: reflect egress, clear conn """ + self.run_clear_conn_test(AF_INET6, 1) + + def test_1006_clear_conn_test(self): + """ IPv6: reflect ingress, clear conn """ + self.run_clear_conn_test(AF_INET6, 0) + + def test_1011_active_conn_test(self): + """ IPv6: Idle conn behind active conn, reflect on ingress """ + self.run_active_conn_test(AF_INET6, 0) + + def test_1012_active_conn_test(self): + """ IPv6: Idle conn behind active conn, reflect on egress """ + self.run_active_conn_test(AF_INET6, 1) + + def test_2000_prepare_for_tcp_test(self): + """ Prepare for TCP session tests """ + # ensure the session hangs on if it gets treated as UDP + self.vapi.ppcli("set acl-plugin session timeout udp idle 200") + # let the TCP connection time out at 5 seconds + self.vapi.ppcli("set acl-plugin session timeout tcp idle 10") + self.vapi.ppcli("set acl-plugin session timeout tcp transient 1") + + def test_2001_tcp_transient_conn_test(self): + """ IPv4: transient TCP session (incomplete 3WHS), ref. on ingress """ + self.run_tcp_transient_setup_conn_test(AF_INET, 0) + + def test_2002_tcp_transient_conn_test(self): + """ IPv4: transient TCP session (incomplete 3WHS), ref. on egress """ + self.run_tcp_transient_setup_conn_test(AF_INET, 1) + + def test_2003_tcp_transient_conn_test(self): + """ IPv4: established TCP session (complete 3WHS), ref. on ingress """ + self.run_tcp_established_conn_test(AF_INET, 0) + + def test_2004_tcp_transient_conn_test(self): + """ IPv4: established TCP session (complete 3WHS), ref. on egress """ + self.run_tcp_established_conn_test(AF_INET, 1) + + def test_2005_tcp_transient_teardown_conn_test(self): + """ IPv4: transient TCP session (3WHS,ACK,FINACK), ref. on ingress """ + self.run_tcp_transient_teardown_conn_test(AF_INET, 0) + + def test_2006_tcp_transient_teardown_conn_test(self): + """ IPv4: transient TCP session (3WHS,ACK,FINACK), ref. on egress """ + self.run_tcp_transient_teardown_conn_test(AF_INET, 1) + + def test_3001_tcp_transient_conn_test(self): + """ IPv6: transient TCP session (incomplete 3WHS), ref. on ingress """ + self.run_tcp_transient_setup_conn_test(AF_INET6, 0) + + def test_3002_tcp_transient_conn_test(self): + """ IPv6: transient TCP session (incomplete 3WHS), ref. on egress """ + self.run_tcp_transient_setup_conn_test(AF_INET6, 1) + + def test_3003_tcp_transient_conn_test(self): + """ IPv6: established TCP session (complete 3WHS), ref. on ingress """ + self.run_tcp_established_conn_test(AF_INET6, 0) + + def test_3004_tcp_transient_conn_test(self): + """ IPv6: established TCP session (complete 3WHS), ref. on egress """ + self.run_tcp_established_conn_test(AF_INET6, 1) + + def test_3005_tcp_transient_teardown_conn_test(self): + """ IPv6: transient TCP session (3WHS,ACK,FINACK), ref. on ingress """ + self.run_tcp_transient_teardown_conn_test(AF_INET6, 0) + + def test_3006_tcp_transient_teardown_conn_test(self): + """ IPv6: transient TCP session (3WHS,ACK,FINACK), ref. on egress """ + self.run_tcp_transient_teardown_conn_test(AF_INET6, 1) diff --git a/test/test_acl_plugin_l2l3.py b/test/test_acl_plugin_l2l3.py new file mode 100644 index 00000000..04f91cfc --- /dev/null +++ b/test/test_acl_plugin_l2l3.py @@ -0,0 +1,686 @@ +#!/usr/bin/env python +"""ACL IRB Test Case HLD: + +**config** + - L2 MAC learning enabled in l2bd + - 2 routed interfaces untagged, bvi (Bridge Virtual Interface) + - 2 bridged interfaces in l2bd with bvi + +**test** + - sending ip4 eth pkts between routed interfaces + - 2 routed interfaces + - 2 bridged interfaces + + - 64B, 512B, 1518B, 9200B (ether_size) + + - burst of pkts per interface + - 257pkts per burst + - routed pkts hitting different FIB entries + - bridged pkts hitting different MAC entries + +**verify** + - all packets received correctly + +""" + +import unittest +from socket import inet_pton, AF_INET, AF_INET6 +from random import choice +from pprint import pprint + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP, ICMP, TCP +from scapy.layers.inet6 import IPv6, ICMPv6Unknown, ICMPv6EchoRequest +from scapy.layers.inet6 import ICMPv6EchoReply, IPv6ExtHdrRouting +from scapy.layers.inet6 import IPv6ExtHdrFragment + +from framework import VppTestCase, VppTestRunner +import time + + +class TestIpIrb(VppTestCase): + """IRB Test Case""" + + @classmethod + def setUpClass(cls): + """ + #. Create BD with MAC learning enabled and put interfaces to this BD. + #. Configure IPv4 addresses on loopback interface and routed interface. + #. Configure MAC address binding to IPv4 neighbors on loop0. + #. Configure MAC address on pg2. + #. Loopback BVI interface has remote hosts, one half of hosts are + behind pg0 second behind pg1. + """ + super(TestIpIrb, cls).setUpClass() + + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] # packet sizes + cls.bd_id = 10 + cls.remote_hosts_count = 250 + + # create 3 pg interfaces, 1 loopback interface + cls.create_pg_interfaces(range(3)) + cls.create_loopback_interfaces(range(1)) + + cls.interfaces = list(cls.pg_interfaces) + cls.interfaces.extend(cls.lo_interfaces) + + for i in cls.interfaces: + i.admin_up() + + # Create BD with MAC learning enabled and put interfaces to this BD + cls.vapi.sw_interface_set_l2_bridge( + cls.loop0.sw_if_index, bd_id=cls.bd_id, bvi=1) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg0.sw_if_index, bd_id=cls.bd_id) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg1.sw_if_index, bd_id=cls.bd_id) + + # Configure IPv4 addresses on loopback interface and routed interface + cls.loop0.config_ip4() + cls.loop0.config_ip6() + cls.pg2.config_ip4() + cls.pg2.config_ip6() + + # Configure MAC address binding to IPv4 neighbors on loop0 + cls.loop0.generate_remote_hosts(cls.remote_hosts_count) + cls.loop0.configure_ipv4_neighbors() + cls.loop0.configure_ipv6_neighbors() + # configure MAC address on pg2 + cls.pg2.resolve_arp() + cls.pg2.resolve_ndp() + + cls.WITHOUT_EH = False + cls.WITH_EH = True + + # Loopback BVI interface has remote hosts, one half of hosts are behind + # pg0 second behind pg1 + half = cls.remote_hosts_count // 2 + cls.pg0.remote_hosts = cls.loop0.remote_hosts[:half] + cls.pg1.remote_hosts = cls.loop0.remote_hosts[half:] + + def tearDown(self): + """Run standard test teardown and log ``show l2patch``, + ``show l2fib verbose``,``show bridge-domain <bd_id> detail``, + ``show ip arp``. + """ + super(TestIpIrb, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show l2patch")) + self.logger.info(self.vapi.cli("show classify tables")) + self.logger.info(self.vapi.cli("show vlib graph")) + self.logger.info(self.vapi.cli("show l2fib verbose")) + self.logger.info(self.vapi.cli("show bridge-domain %s detail" % + self.bd_id)) + self.logger.info(self.vapi.cli("show ip arp")) + self.logger.info(self.vapi.cli("show ip6 neighbors")) + self.logger.info(self.vapi.cli("show acl-plugin sessions")) + self.logger.info(self.vapi.cli("show acl-plugin acl")) + self.logger.info(self.vapi.cli("show acl-plugin interface")) + self.logger.info(self.vapi.cli("show acl-plugin tables")) + + def create_stream(self, src_ip_if, dst_ip_if, reverse, packet_sizes, + is_ip6, expect_blocked, expect_established, + add_extension_header): + pkts = [] + rules = [] + permit_rules = [] + permit_and_reflect_rules = [] + total_packet_count = 8 + for i in range(0, total_packet_count): + modulo = (i//2) % 2 + can_reflect_this_packet = (modulo == 0) + is_permit = i % 2 + remote_dst_index = i % len(dst_ip_if.remote_hosts) + remote_dst_host = dst_ip_if.remote_hosts[remote_dst_index] + if is_permit == 1: + info = self.create_packet_info(src_ip_if, dst_ip_if) + payload = self.info_to_payload(info) + else: + to_be_blocked = False + if (expect_blocked and not expect_established): + to_be_blocked = True + if (not can_reflect_this_packet): + to_be_blocked = True + if to_be_blocked: + payload = "to be blocked" + else: + info = self.create_packet_info(src_ip_if, dst_ip_if) + payload = self.info_to_payload(info) + if reverse: + dst_mac = 'de:ad:00:00:00:00' + src_mac = remote_dst_host._mac + dst_ip6 = src_ip_if.remote_ip6 + src_ip6 = remote_dst_host.ip6 + dst_ip4 = src_ip_if.remote_ip4 + src_ip4 = remote_dst_host.ip4 + dst_l4 = 1234 + i + src_l4 = 4321 + i + else: + dst_mac = src_ip_if.local_mac + src_mac = src_ip_if.remote_mac + src_ip6 = src_ip_if.remote_ip6 + dst_ip6 = remote_dst_host.ip6 + src_ip4 = src_ip_if.remote_ip4 + dst_ip4 = remote_dst_host.ip4 + src_l4 = 1234 + i + dst_l4 = 4321 + i + + # default ULP should be something we do not use in tests + ulp_l4 = TCP(sport=src_l4, dport=dst_l4) + # potentially a chain of protocols leading to ULP + ulp = ulp_l4 + + if can_reflect_this_packet: + if is_ip6: + ulp_l4 = UDP(sport=src_l4, dport=dst_l4) + if add_extension_header: + # prepend some extension headers + ulp = (IPv6ExtHdrRouting() / IPv6ExtHdrRouting() / + IPv6ExtHdrFragment(offset=0, m=1) / ulp_l4) + # uncomment below to test invalid ones + # ulp = IPv6ExtHdrRouting(len = 200) / ulp_l4 + else: + ulp = ulp_l4 + p = (Ether(dst=dst_mac, src=src_mac) / + IPv6(src=src_ip6, dst=dst_ip6) / + ulp / + Raw(payload)) + else: + ulp_l4 = UDP(sport=src_l4, dport=dst_l4) + # IPv4 does not allow extension headers, + # but we rather make it a first fragment + flags = 1 if add_extension_header else 0 + ulp = ulp_l4 + p = (Ether(dst=dst_mac, src=src_mac) / + IP(src=src_ip4, dst=dst_ip4, frag=0, flags=flags) / + ulp / + Raw(payload)) + elif modulo == 1: + if is_ip6: + ulp_l4 = ICMPv6Unknown(type=128 + (i % 2), code=i % 2) + ulp = ulp_l4 + p = (Ether(dst=dst_mac, src=src_mac) / + IPv6(src=src_ip6, dst=dst_ip6) / + ulp / + Raw(payload)) + else: + ulp_l4 = ICMP(type=8 + (i % 2), code=i % 2) + ulp = ulp_l4 + p = (Ether(dst=dst_mac, src=src_mac) / + IP(src=src_ip4, dst=dst_ip4) / + ulp / + Raw(payload)) + + if i % 2 == 1: + info.data = p.copy() + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + + rule_family = AF_INET6 if p.haslayer(IPv6) else AF_INET + rule_prefix_len = 128 if p.haslayer(IPv6) else 32 + rule_l3_layer = IPv6 if p.haslayer(IPv6) else IP + + if p.haslayer(UDP): + rule_l4_sport = p[UDP].sport + rule_l4_dport = p[UDP].dport + else: + if p.haslayer(ICMP): + rule_l4_sport = p[ICMP].type + rule_l4_dport = p[ICMP].code + else: + rule_l4_sport = p[ICMPv6Unknown].type + rule_l4_dport = p[ICMPv6Unknown].code + if p.haslayer(IPv6): + rule_l4_proto = ulp_l4.overload_fields[IPv6]['nh'] + else: + rule_l4_proto = p[IP].proto + + new_rule = { + 'is_permit': is_permit, + 'is_ipv6': p.haslayer(IPv6), + 'src_ip_addr': inet_pton(rule_family, + p[rule_l3_layer].src), + 'src_ip_prefix_len': rule_prefix_len, + 'dst_ip_addr': inet_pton(rule_family, + p[rule_l3_layer].dst), + 'dst_ip_prefix_len': rule_prefix_len, + 'srcport_or_icmptype_first': rule_l4_sport, + 'srcport_or_icmptype_last': rule_l4_sport, + 'dstport_or_icmpcode_first': rule_l4_dport, + 'dstport_or_icmpcode_last': rule_l4_dport, + 'proto': rule_l4_proto, + } + rules.append(new_rule) + new_rule_permit = new_rule.copy() + new_rule_permit['is_permit'] = 1 + permit_rules.append(new_rule_permit) + + new_rule_permit_and_reflect = new_rule.copy() + if can_reflect_this_packet: + new_rule_permit_and_reflect['is_permit'] = 2 + else: + new_rule_permit_and_reflect['is_permit'] = is_permit + permit_and_reflect_rules.append(new_rule_permit_and_reflect) + + return {'stream': pkts, + 'rules': rules, + 'permit_rules': permit_rules, + 'permit_and_reflect_rules': permit_and_reflect_rules} + + def verify_capture(self, dst_ip_if, src_ip_if, capture, reverse): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + + dst_ip_sw_if_index = dst_ip_if.sw_if_index + return + + for packet in capture: + l3 = IP if packet.haslayer(IP) else IPv6 + ip = packet[l3] + if packet.haslayer(UDP): + l4 = UDP + else: + if packet.haslayer(ICMP): + l4 = ICMP + else: + l4 = ICMPv6Unknown + + # Scapy IPv6 stuff is too smart for its own good. + # So we do this and coerce the ICMP into unknown type + if packet.haslayer(UDP): + data = str(packet[UDP][Raw]) + else: + if l3 == IP: + data = str(ICMP(str(packet[l3].payload))[Raw]) + else: + data = str(ICMPv6Unknown(str(packet[l3].payload)).msgbody) + udp_or_icmp = packet[l3].payload + payload_info = self.payload_to_info(data) + packet_index = payload_info.index + + self.assertEqual(payload_info.dst, dst_ip_sw_if_index) + + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_ip_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + self.assertTrue(next_info is not None) + + # MAC: src, dst + if not reverse: + self.assertEqual(packet.src, dst_ip_if.local_mac) + host = dst_ip_if.host_by_mac(packet.dst) + + # IP: src, dst + # self.assertEqual(ip.src, src_ip_if.remote_ip4) + if saved_packet is not None: + self.assertEqual(ip.src, saved_packet[l3].src) + self.assertEqual(ip.dst, saved_packet[l3].dst) + if l4 == UDP: + self.assertEqual(udp_or_icmp.sport, saved_packet[l4].sport) + self.assertEqual(udp_or_icmp.dport, saved_packet[l4].dport) + else: + print("Saved packet is none") + # self.assertEqual(ip.dst, host.ip4) + + # UDP: + + def create_acls_for_a_stream(self, stream_dict, + test_l2_action, is_reflect): + r = stream_dict['rules'] + r_permit = stream_dict['permit_rules'] + r_permit_reflect = stream_dict['permit_and_reflect_rules'] + r_action = r_permit_reflect if is_reflect else r + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=r_action, + tag="act. acl") + action_acl_index = reply.acl_index + reply = self.vapi.acl_add_replace(acl_index=4294967295, r=r_permit, + tag="perm. acl") + permit_acl_index = reply.acl_index + return {'L2': action_acl_index if test_l2_action else permit_acl_index, + 'L3': permit_acl_index if test_l2_action else action_acl_index, + 'permit': permit_acl_index, 'action': action_acl_index} + + def apply_acl_ip46_x_to_y(self, bridged_to_routed, test_l2_deny, + is_ip6, is_reflect, add_eh): + """ Apply the ACLs + """ + self.reset_packet_infos() + stream_dict = self.create_stream( + self.pg2, self.loop0, + bridged_to_routed, + self.pg_if_packet_sizes, is_ip6, + not is_reflect, False, add_eh) + stream = stream_dict['stream'] + acl_idx = self.create_acls_for_a_stream(stream_dict, test_l2_deny, + is_reflect) + n_input_l3 = 0 if bridged_to_routed else 1 + n_input_l2 = 1 if bridged_to_routed else 0 + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg2.sw_if_index, + n_input=n_input_l3, + acls=[acl_idx['L3']]) + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=n_input_l2, + acls=[acl_idx['L2']]) + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg1.sw_if_index, + n_input=n_input_l2, + acls=[acl_idx['L2']]) + + def apply_acl_ip46_both_directions_reflect(self, + primary_is_bridged_to_routed, + reflect_on_l2, is_ip6, add_eh): + primary_is_routed_to_bridged = not primary_is_bridged_to_routed + self.reset_packet_infos() + stream_dict_fwd = self.create_stream(self.pg2, self.loop0, + primary_is_bridged_to_routed, + self.pg_if_packet_sizes, is_ip6, + False, False, add_eh) + acl_idx_fwd = self.create_acls_for_a_stream(stream_dict_fwd, + reflect_on_l2, True) + + stream_dict_rev = self.create_stream(self.pg2, self.loop0, + not primary_is_bridged_to_routed, + self.pg_if_packet_sizes, is_ip6, + True, True, add_eh) + # We want the primary action to be "deny" rather than reflect + acl_idx_rev = self.create_acls_for_a_stream(stream_dict_rev, + reflect_on_l2, False) + + if primary_is_bridged_to_routed: + inbound_l2_acl = acl_idx_fwd['L2'] + else: + inbound_l2_acl = acl_idx_rev['L2'] + + if primary_is_routed_to_bridged: + outbound_l2_acl = acl_idx_fwd['L2'] + else: + outbound_l2_acl = acl_idx_rev['L2'] + + if primary_is_routed_to_bridged: + inbound_l3_acl = acl_idx_fwd['L3'] + else: + inbound_l3_acl = acl_idx_rev['L3'] + + if primary_is_bridged_to_routed: + outbound_l3_acl = acl_idx_fwd['L3'] + else: + outbound_l3_acl = acl_idx_rev['L3'] + + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg2.sw_if_index, + n_input=1, + acls=[inbound_l3_acl, + outbound_l3_acl]) + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg0.sw_if_index, + n_input=1, + acls=[inbound_l2_acl, + outbound_l2_acl]) + self.vapi.acl_interface_set_acl_list(sw_if_index=self.pg1.sw_if_index, + n_input=1, + acls=[inbound_l2_acl, + outbound_l2_acl]) + + def apply_acl_ip46_routed_to_bridged(self, test_l2_deny, is_ip6, + is_reflect, add_eh): + self.apply_acl_ip46_x_to_y(False, test_l2_deny, is_ip6, + is_reflect, add_eh) + + def apply_acl_ip46_bridged_to_routed(self, test_l2_deny, is_ip6, + is_reflect, add_eh): + self.apply_acl_ip46_x_to_y(True, test_l2_deny, is_ip6, + is_reflect, add_eh) + + def run_traffic_ip46_x_to_y(self, bridged_to_routed, + test_l2_deny, is_ip6, + is_reflect, is_established, add_eh): + self.reset_packet_infos() + stream_dict = self.create_stream(self.pg2, self.loop0, + bridged_to_routed, + self.pg_if_packet_sizes, is_ip6, + not is_reflect, is_established, + add_eh) + stream = stream_dict['stream'] + + tx_if = self.pg0 if bridged_to_routed else self.pg2 + rx_if = self.pg2 if bridged_to_routed else self.pg0 + + tx_if.add_stream(stream) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + packet_count = self.get_packet_count_for_if_idx(self.loop0.sw_if_index) + rcvd1 = rx_if.get_capture(packet_count) + self.verify_capture(self.loop0, self.pg2, rcvd1, bridged_to_routed) + + def run_traffic_ip46_routed_to_bridged(self, test_l2_deny, is_ip6, + is_reflect, is_established, add_eh): + self.run_traffic_ip46_x_to_y(False, test_l2_deny, is_ip6, + is_reflect, is_established, add_eh) + + def run_traffic_ip46_bridged_to_routed(self, test_l2_deny, is_ip6, + is_reflect, is_established, add_eh): + self.run_traffic_ip46_x_to_y(True, test_l2_deny, is_ip6, + is_reflect, is_established, add_eh) + + def run_test_ip46_routed_to_bridged(self, test_l2_deny, + is_ip6, is_reflect, add_eh): + self.apply_acl_ip46_routed_to_bridged(test_l2_deny, + is_ip6, is_reflect, add_eh) + self.run_traffic_ip46_routed_to_bridged(test_l2_deny, is_ip6, + is_reflect, False, add_eh) + + def run_test_ip46_bridged_to_routed(self, test_l2_deny, + is_ip6, is_reflect, add_eh): + self.apply_acl_ip46_bridged_to_routed(test_l2_deny, + is_ip6, is_reflect, add_eh) + self.run_traffic_ip46_bridged_to_routed(test_l2_deny, is_ip6, + is_reflect, False, add_eh) + + def run_test_ip46_routed_to_bridged_and_back(self, test_l2_action, + is_ip6, add_eh): + self.apply_acl_ip46_both_directions_reflect(False, test_l2_action, + is_ip6, add_eh) + self.run_traffic_ip46_routed_to_bridged(test_l2_action, is_ip6, + True, False, add_eh) + self.run_traffic_ip46_bridged_to_routed(test_l2_action, is_ip6, + False, True, add_eh) + + def run_test_ip46_bridged_to_routed_and_back(self, test_l2_action, + is_ip6, add_eh): + self.apply_acl_ip46_both_directions_reflect(True, test_l2_action, + is_ip6, add_eh) + self.run_traffic_ip46_bridged_to_routed(test_l2_action, is_ip6, + True, False, add_eh) + self.run_traffic_ip46_routed_to_bridged(test_l2_action, is_ip6, + False, True, add_eh) + + def test_0000_ip6_irb_1(self): + """ ACL plugin prepare""" + if not self.vpp_dead: + cmd = "set acl-plugin session timeout udp idle 2000" + self.logger.info(self.vapi.ppcli(cmd)) + # uncomment to not skip past the routing header + # and watch the EH tests fail + # self.logger.info(self.vapi.ppcli( + # "set acl-plugin skip-ipv6-extension-header 43 0")) + # uncomment to test the session limit (stateful tests will fail) + # self.logger.info(self.vapi.ppcli( + # "set acl-plugin session table max-entries 1")) + # new datapath is the default, but just in case + # self.logger.info(self.vapi.ppcli( + # "set acl-plugin l2-datapath new")) + # If you want to see some tests fail, uncomment the next line + # self.logger.info(self.vapi.ppcli( + # "set acl-plugin l2-datapath old")) + + def test_0001_ip6_irb_1(self): + """ ACL IPv6 routed -> bridged, L2 ACL deny""" + self.run_test_ip46_routed_to_bridged(True, True, False, + self.WITHOUT_EH) + + def test_0002_ip6_irb_1(self): + """ ACL IPv6 routed -> bridged, L3 ACL deny""" + self.run_test_ip46_routed_to_bridged(False, True, False, + self.WITHOUT_EH) + + def test_0003_ip4_irb_1(self): + """ ACL IPv4 routed -> bridged, L2 ACL deny""" + self.run_test_ip46_routed_to_bridged(True, False, False, + self.WITHOUT_EH) + + def test_0004_ip4_irb_1(self): + """ ACL IPv4 routed -> bridged, L3 ACL deny""" + self.run_test_ip46_routed_to_bridged(False, False, False, + self.WITHOUT_EH) + + def test_0005_ip6_irb_1(self): + """ ACL IPv6 bridged -> routed, L2 ACL deny """ + self.run_test_ip46_bridged_to_routed(True, True, False, + self.WITHOUT_EH) + + def test_0006_ip6_irb_1(self): + """ ACL IPv6 bridged -> routed, L3 ACL deny """ + self.run_test_ip46_bridged_to_routed(False, True, False, + self.WITHOUT_EH) + + def test_0007_ip6_irb_1(self): + """ ACL IPv4 bridged -> routed, L2 ACL deny """ + self.run_test_ip46_bridged_to_routed(True, False, False, + self.WITHOUT_EH) + + def test_0008_ip6_irb_1(self): + """ ACL IPv4 bridged -> routed, L3 ACL deny """ + self.run_test_ip46_bridged_to_routed(False, False, False, + self.WITHOUT_EH) + + # Stateful ACL tests + def test_0101_ip6_irb_1(self): + """ ACL IPv6 routed -> bridged, L2 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(True, True, + self.WITHOUT_EH) + + def test_0102_ip6_irb_1(self): + """ ACL IPv6 bridged -> routed, L2 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(True, True, + self.WITHOUT_EH) + + def test_0103_ip6_irb_1(self): + """ ACL IPv4 routed -> bridged, L2 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(True, False, + self.WITHOUT_EH) + + def test_0104_ip6_irb_1(self): + """ ACL IPv4 bridged -> routed, L2 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(True, False, + self.WITHOUT_EH) + + def test_0111_ip6_irb_1(self): + """ ACL IPv6 routed -> bridged, L3 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(False, True, + self.WITHOUT_EH) + + def test_0112_ip6_irb_1(self): + """ ACL IPv6 bridged -> routed, L3 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(False, True, + self.WITHOUT_EH) + + def test_0113_ip6_irb_1(self): + """ ACL IPv4 routed -> bridged, L3 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(False, False, + self.WITHOUT_EH) + + def test_0114_ip6_irb_1(self): + """ ACL IPv4 bridged -> routed, L3 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(False, False, + self.WITHOUT_EH) + + # A block of tests with extension headers + + def test_1001_ip6_irb_1(self): + """ ACL IPv6+EH routed -> bridged, L2 ACL deny""" + self.run_test_ip46_routed_to_bridged(True, True, False, + self.WITH_EH) + + def test_1002_ip6_irb_1(self): + """ ACL IPv6+EH routed -> bridged, L3 ACL deny""" + self.run_test_ip46_routed_to_bridged(False, True, False, + self.WITH_EH) + + def test_1005_ip6_irb_1(self): + """ ACL IPv6+EH bridged -> routed, L2 ACL deny """ + self.run_test_ip46_bridged_to_routed(True, True, False, + self.WITH_EH) + + def test_1006_ip6_irb_1(self): + """ ACL IPv6+EH bridged -> routed, L3 ACL deny """ + self.run_test_ip46_bridged_to_routed(False, True, False, + self.WITH_EH) + + def test_1101_ip6_irb_1(self): + """ ACL IPv6+EH routed -> bridged, L2 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(True, True, + self.WITH_EH) + + def test_1102_ip6_irb_1(self): + """ ACL IPv6+EH bridged -> routed, L2 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(True, True, + self.WITH_EH) + + def test_1111_ip6_irb_1(self): + """ ACL IPv6+EH routed -> bridged, L3 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(False, True, + self.WITH_EH) + + def test_1112_ip6_irb_1(self): + """ ACL IPv6+EH bridged -> routed, L3 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(False, True, + self.WITH_EH) + + # IPv4 with "MF" bit set + + def test_1201_ip6_irb_1(self): + """ ACL IPv4+MF routed -> bridged, L2 ACL deny""" + self.run_test_ip46_routed_to_bridged(True, False, False, + self.WITH_EH) + + def test_1202_ip6_irb_1(self): + """ ACL IPv4+MF routed -> bridged, L3 ACL deny""" + self.run_test_ip46_routed_to_bridged(False, False, False, + self.WITH_EH) + + def test_1205_ip6_irb_1(self): + """ ACL IPv4+MF bridged -> routed, L2 ACL deny """ + self.run_test_ip46_bridged_to_routed(True, False, False, + self.WITH_EH) + + def test_1206_ip6_irb_1(self): + """ ACL IPv4+MF bridged -> routed, L3 ACL deny """ + self.run_test_ip46_bridged_to_routed(False, False, False, + self.WITH_EH) + + def test_1301_ip6_irb_1(self): + """ ACL IPv4+MF routed -> bridged, L2 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(True, False, + self.WITH_EH) + + def test_1302_ip6_irb_1(self): + """ ACL IPv4+MF bridged -> routed, L2 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(True, False, + self.WITH_EH) + + def test_1311_ip6_irb_1(self): + """ ACL IPv4+MF routed -> bridged, L3 ACL permit+reflect""" + self.run_test_ip46_routed_to_bridged_and_back(False, False, + self.WITH_EH) + + def test_1312_ip6_irb_1(self): + """ ACL IPv4+MF bridged -> routed, L3 ACL permit+reflect""" + self.run_test_ip46_bridged_to_routed_and_back(False, False, + self.WITH_EH) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_acl_plugin_macip.py b/test/test_acl_plugin_macip.py new file mode 100644 index 00000000..f10d6cc6 --- /dev/null +++ b/test/test_acl_plugin_macip.py @@ -0,0 +1,961 @@ +#!/usr/bin/env python +"""ACL plugin - MACIP tests +""" +import random +import re +import unittest + +from socket import inet_ntop, inet_pton, AF_INET, AF_INET6 +from struct import * +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP, TCP +from scapy.layers.inet6 import IPv6 + +from framework import VppTestCase, VppTestRunner, running_extended_tests +from vpp_lo_interface import VppLoInterface + + +class TestMACIP(VppTestCase): + """MACIP Test Case""" + + DEBUG = False + + BRIDGED = True + ROUTED = False + + IS_IP4 = False + IS_IP6 = True + + # rule types + DENY = 0 + PERMIT = 1 + + # ACL types + EXACT_IP = 1 + SUBNET_IP = 2 + WILD_IP = 3 + + EXACT_MAC = 1 + WILD_MAC = 2 + OUI_MAC = 3 + + ACLS = [] + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestMACIP, cls).setUpClass() + + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] # packet sizes + cls.bd_id = 10 + cls.remote_hosts_count = 250 + + try: + # create 3 pg interfaces, 1 loopback interface + cls.create_pg_interfaces(range(3)) + cls.create_loopback_interfaces(range(1)) + + cls.interfaces = list(cls.pg_interfaces) + cls.interfaces.extend(cls.lo_interfaces) + + for i in cls.interfaces: + i.admin_up() + + # Create BD with MAC learning enabled and put interfaces to this BD + cls.vapi.sw_interface_set_l2_bridge( + cls.loop0.sw_if_index, bd_id=cls.bd_id, bvi=1) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg0.sw_if_index, bd_id=cls.bd_id) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg1.sw_if_index, bd_id=cls.bd_id) + + # Configure IPv4 addresses on loop interface and routed interface + cls.loop0.config_ip4() + cls.loop0.config_ip6() + cls.pg2.config_ip4() + cls.pg2.config_ip6() + + # Configure MAC address binding to IPv4 neighbors on loop0 + cls.loop0.generate_remote_hosts(cls.remote_hosts_count) + # Modify host mac addresses to have different OUI parts + for i in range(2, cls.remote_hosts_count + 2): + mac = cls.loop0.remote_hosts[i-2]._mac.split(':') + mac[2] = format(int(mac[2], 16) + i, "02x") + cls.loop0.remote_hosts[i - 2]._mac = ":".join(mac) + + cls.loop0.configure_ipv4_neighbors() + cls.loop0.configure_ipv6_neighbors() + # configure MAC address on pg2 + cls.pg2.resolve_arp() + cls.pg2.resolve_ndp() + + # Loopback BVI interface has remote hosts + # one half of hosts are behind pg0 second behind pg1 + half = cls.remote_hosts_count // 2 + cls.pg0.remote_hosts = cls.loop0.remote_hosts[:half] + cls.pg1.remote_hosts = cls.loop0.remote_hosts[half:] + + except Exception: + super(TestMACIP, cls).tearDownClass() + raise + + def setUp(self): + super(TestMACIP, self).setUp() + self.reset_packet_infos() + del self.ACLS[:] + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestMACIP, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show interface address")) + self.logger.info(self.vapi.ppcli("show hardware")) + self.logger.info(self.vapi.ppcli("sh acl-plugin macip acl")) + self.logger.info(self.vapi.ppcli("sh acl-plugin macip interface")) + self.logger.info(self.vapi.ppcli("sh classify tables verbose")) + # print self.vapi.ppcli("show interface address") + # print self.vapi.ppcli("show hardware") + # print self.vapi.ppcli("sh acl-plugin macip interface") + # print self.vapi.ppcli("sh acl-plugin macip acl") + self.delete_acls() + + def macip_acl_dump_debug(self): + acls = self.vapi.macip_acl_dump() + if self.DEBUG: + for acl in acls: + print "ACL #"+str(acl.acl_index) + for r in acl.r: + rule = "ACTION" + if r.is_permit == 1: + rule = "PERMIT" + elif r.is_permit == 0: + rule = "DENY " + print " IP6" if r.is_ipv6 else " IP4", \ + rule, \ + r.src_mac.encode('hex'), \ + r.src_mac_mask.encode('hex'),\ + unpack('<16B', r.src_ip_addr), \ + r.src_ip_prefix_len + return acls + + def create_rules(self, mac_type=EXACT_MAC, ip_type=EXACT_IP, + acl_count=1, rules_count=[1]): + acls = [] + src_mac = int("220000dead00", 16) + for acl in range(2, (acl_count+1) * 2): + rules = [] + host = random.choice(self.loop0.remote_hosts) + is_ip6 = acl % 2 + ip4 = host.ip4.split('.') + ip6 = list(unpack('<16B', inet_pton(AF_INET6, host.ip6))) + + if ip_type == self.EXACT_IP: + prefix_len4 = 32 + prefix_len6 = 128 + elif ip_type == self.WILD_IP: + ip4 = [0, 0, 0, 0] + ip6 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + prefix_len4 = 0 + prefix_len6 = 0 + rules_count[(acl / 2) - 1] = 1 + else: + prefix_len4 = 24 + prefix_len6 = 64 + + if mac_type == self.EXACT_MAC: + mask = "ff:ff:ff:ff:ff:ff" + elif mac_type == self.WILD_MAC: + mask = "00:00:00:00:00:00" + elif mac_type == self.OUI_MAC: + mask = "ff:ff:ff:00:00:00" + else: + mask = "ff:ff:ff:ff:ff:00" + + ip = ip6 if is_ip6 else ip4 + ip_len = prefix_len6 if is_ip6 else prefix_len4 + + for i in range(0, rules_count[(acl / 2) - 1]): + src_mac += 16777217 + if mac_type == self.WILD_MAC: + mac = "00:00:00:00:00:00" + elif mac_type == self.OUI_MAC: + mac = ':'.join(re.findall('..', '{:02x}'.format( + src_mac))[:3])+":00:00:00" + else: + mac = ':'.join(re.findall('..', '{:02x}'.format(src_mac))) + + if ip_type == self.EXACT_IP: + ip4[3] = random.randint(100, 200) + ip6[15] = random.randint(100, 200) + elif ip_type == self.SUBNET_IP: + ip4[2] = random.randint(100, 200) + ip4[3] = 0 + ip6[8] = random.randint(100, 200) + ip6[15] = 0 + ip_pack = '' + for j in range(0, len(ip)): + ip_pack += pack('<B', int(ip[j])) + + rule = ({'is_permit': self.PERMIT, + 'is_ipv6': is_ip6, + 'src_ip_addr': ip_pack, + 'src_ip_prefix_len': ip_len, + 'src_mac': mac.replace(':', '').decode('hex'), + 'src_mac_mask': mask.replace(':', '').decode('hex')}) + rules.append(rule) + if ip_type == self.WILD_IP: + break + + acls.append(rules) + src_mac += 1099511627776 + return acls + + def apply_rules(self, acls): + for acl in acls: + reply = self.vapi.macip_acl_add(acl) + self.assertEqual(reply.retval, 0) + self.ACLS.append(reply.acl_index) + + def verify_acls(self, acl_count, rules_count, expected_count=2): + reply = self.macip_acl_dump_debug() + for acl in range(2, (acl_count+1) * 2): + self.assertEqual(reply[acl - 2].count, rules_count[acl/2-1]) + + self.vapi.macip_acl_interface_get() + + self.vapi.macip_acl_interface_add_del(sw_if_index=0, acl_index=0) + self.vapi.macip_acl_interface_add_del(sw_if_index=1, acl_index=1) + + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, expected_count) + + def delete_acls(self): + for acl in range(len(self.ACLS)-1, -1, -1): + self.vapi.macip_acl_del(self.ACLS[acl]) + + reply = self.vapi.macip_acl_dump() + self.assertEqual(len(reply), 0) + + def create_stream(self, mac_type, ip_type, packet_count, + src_ip_if, dst_ip_if, bridged_routed, is_ip6): + # exact MAC and exact IP + # exact MAC and subnet of IPs + # exact MAC and wildcard IP + # wildcard MAC and exact IP + # wildcard MAC and subnet of IPs + # wildcard MAC and wildcard IP + # OUI restricted MAC and exact IP + # OUI restricted MAC and subnet of IPs + # OUI restricted MAC and wildcard IP + + packets = [] + rules = [] + ip_permit = "" + mac_permit = "" + dst_mac = "" + mac_rule = "00:00:00:00:00:00" + mac_mask = "00:00:00:00:00:00" + for p in range(0, packet_count): + remote_dst_index = p % len(dst_ip_if.remote_hosts) + remote_dst_host = dst_ip_if.remote_hosts[remote_dst_index] + + dst_port = 1234 + p + src_port = 4321 + p + is_permit = self.PERMIT if p % 3 == 0 else self.DENY + denyMAC = True if not is_permit and p % 3 == 1 else False + denyIP = True if not is_permit and p % 3 == 2 else False + if not is_permit and ip_type == self.WILD_IP: + denyMAC = True + if not is_permit and mac_type == self.WILD_MAC: + denyIP = True + + if bridged_routed == self.BRIDGED: + if is_permit: + src_mac = remote_dst_host._mac + dst_mac = 'de:ad:00:00:00:00' + src_ip4 = remote_dst_host.ip4 + dst_ip4 = src_ip_if.remote_ip4 + src_ip6 = remote_dst_host.ip6 + dst_ip6 = src_ip_if.remote_ip6 + ip_permit = src_ip6 if is_ip6 else src_ip4 + mac_permit = src_mac + if denyMAC: + mac = src_mac.split(':') + mac[0] = format(int(mac[0], 16)+1, "02x") + src_mac = ":".join(mac) + if is_ip6: + src_ip6 = ip_permit + else: + src_ip4 = ip_permit + if denyIP: + if ip_type != self.WILD_IP: + src_mac = mac_permit + src_ip4 = remote_dst_host.ip4 + dst_ip4 = src_ip_if.remote_ip4 + src_ip6 = remote_dst_host.ip6 + dst_ip6 = src_ip_if.remote_ip6 + else: + if is_permit: + src_mac = remote_dst_host._mac + dst_mac = src_ip_if.local_mac + src_ip4 = src_ip_if.remote_ip4 + dst_ip4 = remote_dst_host.ip4 + src_ip6 = src_ip_if.remote_ip6 + dst_ip6 = remote_dst_host.ip6 + ip_permit = src_ip6 if is_ip6 else src_ip4 + mac_permit = src_mac + if denyMAC: + mac = src_mac.split(':') + mac[0] = format(int(mac[0], 16) + 1, "02x") + src_mac = ":".join(mac) + if is_ip6: + src_ip6 = ip_permit + else: + src_ip4 = ip_permit + if denyIP: + src_mac = remote_dst_host._mac + if ip_type != self.WILD_IP: + src_mac = mac_permit + src_ip4 = remote_dst_host.ip4 + dst_ip4 = src_ip_if.remote_ip4 + src_ip6 = remote_dst_host.ip6 + dst_ip6 = src_ip_if.remote_ip6 + + if is_permit: + info = self.create_packet_info(src_ip_if, dst_ip_if) + payload = self.info_to_payload(info) + else: + payload = "to be blocked" + + if mac_type == self.WILD_MAC: + mac = src_mac.split(':') + for i in range(1, 5): + mac[i] = format(random.randint(0, 255), "02x") + src_mac = ":".join(mac) + + # create packet + packet = Ether(src=src_mac, dst=dst_mac) + ip_rule = src_ip6 if is_ip6 else src_ip4 + if is_ip6: + if ip_type != self.EXACT_IP: + sub_ip = list(unpack('<16B', inet_pton(AF_INET6, ip_rule))) + if ip_type == self.WILD_IP: + sub_ip[0] = random.randint(240, 254) + sub_ip[1] = random.randint(230, 239) + sub_ip[14] = random.randint(100, 199) + sub_ip[15] = random.randint(200, 255) + elif ip_type == self.SUBNET_IP: + if denyIP: + sub_ip[2] = str(int(sub_ip[2]) + 1) + sub_ip[14] = random.randint(100, 199) + sub_ip[15] = random.randint(200, 255) + src_ip6 = inet_ntop(AF_INET6, str(bytearray(sub_ip))) + packet /= IPv6(src=src_ip6, dst=dst_ip6) + else: + if ip_type != self.EXACT_IP: + sub_ip = ip_rule.split('.') + if ip_type == self.WILD_IP: + sub_ip[0] = str(random.randint(1, 49)) + sub_ip[1] = str(random.randint(50, 99)) + sub_ip[2] = str(random.randint(100, 199)) + sub_ip[3] = str(random.randint(200, 255)) + elif ip_type == self.SUBNET_IP: + if denyIP: + sub_ip[1] = str(int(sub_ip[1])+1) + sub_ip[2] = str(random.randint(100, 199)) + sub_ip[3] = str(random.randint(200, 255)) + src_ip4 = ".".join(sub_ip) + packet /= IP(src=src_ip4, dst=dst_ip4, frag=0, flags=0) + + packet /= UDP(sport=src_port, dport=dst_port)/Raw(payload) + + packet[Raw].load += " mac:"+src_mac + + size = self.pg_if_packet_sizes[p % len(self.pg_if_packet_sizes)] + self.extend_packet(packet, size) + packets.append(packet) + + # create suitable rule + if mac_type == self.EXACT_MAC: + mac_rule = src_mac + mac_mask = "ff:ff:ff:ff:ff:ff" + elif mac_type == self.WILD_MAC: + mac_rule = "00:00:00:00:00:00" + mac_mask = "00:00:00:00:00:00" + elif mac_type == self.OUI_MAC: + mac = src_mac.split(':') + mac[3] = mac[4] = mac[5] = '00' + mac_rule = ":".join(mac) + mac_mask = "ff:ff:ff:00:00:00" + + if is_ip6: + if ip_type == self.WILD_IP: + ip = "0::0" + else: + ip = src_ip6 + if ip_type == self.SUBNET_IP: + sub_ip = list(unpack('<16B', inet_pton(AF_INET6, ip))) + for i in range(8, 16): + sub_ip[i] = 0 + ip = inet_ntop(AF_INET6, str(bytearray(sub_ip))) + else: + if ip_type == self.WILD_IP: + ip = "0.0.0.0" + else: + ip = src_ip4 + if ip_type == self.SUBNET_IP: + sub_ip = ip.split('.') + sub_ip[2] = sub_ip[3] = '0' + ip = ".".join(sub_ip) + + prefix_len = 128 if is_ip6 else 32 + if ip_type == self.WILD_IP: + prefix_len = 0 + elif ip_type == self.SUBNET_IP: + prefix_len = 64 if is_ip6 else 16 + ip_rule = inet_pton(AF_INET6 if is_ip6 else AF_INET, ip) + + if mac_type == self.WILD_MAC and ip_type == self.WILD_IP and p > 0: + continue + + if is_permit: + rule = ({'is_permit': is_permit, + 'is_ipv6': is_ip6, + 'src_ip_addr': ip_rule, + 'src_ip_prefix_len': prefix_len, + 'src_mac': mac_rule.replace(':', '').decode('hex'), + 'src_mac_mask': mac_mask.replace(':', '').decode( + 'hex')}) + rules.append(rule) + + # deny all other packets + if not (mac_type == self.WILD_MAC and ip_type == self.WILD_IP): + rule = ({'is_permit': 0, + 'is_ipv6': is_ip6, + 'src_ip_addr': "", + 'src_ip_prefix_len': 0, + 'src_mac': "", + 'src_mac_mask': ""}) + rules.append(rule) + + return {'stream': packets, 'rules': rules} + + def verify_capture(self, stream, capture, is_ip6): + p_l3 = IPv6 if is_ip6 else IP + if self.DEBUG: + for p in stream: + print p[Ether].src, p[Ether].dst, p[p_l3].src, p[p_l3].dst + + acls = self.macip_acl_dump_debug() + + # TODO : verify + # for acl in acls: + # for r in acl.r: + # print r.src_mac.encode('hex'), \ + # r.src_mac_mask.encode('hex'),\ + # unpack('<16B', r.src_ip_addr), \ + # r.src_ip_prefix_len + # + # for p in capture: + # print p[Ether].src, p[Ether].dst, p[p_l3].src, p[p_l3].dst + # data = p[Raw].load.split(':',1)[1] + # print p[p_l3].src, data + + def run_traffic(self, mac_type, ip_type, bridged_routed, is_ip6, packets, + do_not_expected_capture=False): + self.reset_packet_infos() + + tx_if = self.pg0 if bridged_routed == self.BRIDGED else self.pg2 + rx_if = self.pg2 if bridged_routed == self.BRIDGED else self.pg0 + + test_dict = self.create_stream(mac_type, ip_type, packets, + self.pg2, self.loop0, + bridged_routed, + is_ip6) + + reply = self.vapi.macip_acl_add(test_dict['rules']) + self.assertEqual(reply.retval, 0) + acl_index = reply.acl_index + + self.vapi.macip_acl_interface_add_del(sw_if_index=tx_if.sw_if_index, + acl_index=acl_index) + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.acls[tx_if.sw_if_index], acl_index) + self.ACLS.append(reply.acls[tx_if.sw_if_index]) + + tx_if.add_stream(test_dict['stream']) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + if do_not_expected_capture: + rx_if.get_capture(0) + else: + packet_count = self.get_packet_count_for_if_idx( + self.loop0.sw_if_index) + if mac_type == self.WILD_MAC and ip_type == self.WILD_IP: + packet_count = packets if bridged_routed else packet_count + capture = rx_if.get_capture(packet_count) + self.verify_capture(test_dict['stream'], capture, is_ip6) + + def run_test_acls(self, mac_type, ip_type, acl_count, + rules_count, traffic=None, ip=None): + self.apply_rules(self.create_rules(mac_type, ip_type, acl_count, + rules_count)) + self.verify_acls(acl_count, rules_count) + + if traffic is not None: + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, traffic, ip, 9) + + def test_acl_bridged_ip4_exactMAC_exactIP(self): + """ IP4 MACIP exactMAC|exactIP ACL bridged traffic + """ + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_exactMAC_exactIP(self): + """ IP6 MACIP exactMAC|exactIP ACL bridged traffic + """ + + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_exactMAC_subnetIP(self): + """ IP4 MACIP exactMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.EXACT_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_exactMAC_subnetIP(self): + """ IP6 MACIP exactMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.EXACT_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_exactMAC_wildIP(self): + """ IP4 MACIP exactMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.EXACT_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_exactMAC_wildIP(self): + """ IP6 MACIP exactMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.EXACT_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_ouiMAC_exactIP(self): + """ IP4 MACIP ouiMAC|exactIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP4, 3) + + def test_acl_bridged_ip6_ouiMAC_exactIP(self): + """ IP6 MACIP oui_MAC|exactIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_ouiMAC_subnetIP(self): + """ IP4 MACIP ouiMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_ouiMAC_subnetIP(self): + """ IP6 MACIP ouiMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_ouiMAC_wildIP(self): + """ IP4 MACIP ouiMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_ouiMAC_wildIP(self): + """ IP6 MACIP ouiMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.OUI_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_ac_bridgedl_ip4_wildMAC_exactIP(self): + """ IP4 MACIP wildcardMAC|exactIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_wildMAC_exactIP(self): + """ IP6 MACIP wildcardMAC|exactIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_wildMAC_subnetIP(self): + """ IP4 MACIP wildcardMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_wildMAC_subnetIP(self): + """ IP6 MACIP wildcardMAC|subnetIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_bridged_ip4_wildMAC_wildIP(self): + """ IP4 MACIP wildcardMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP4, 9) + + def test_acl_bridged_ip6_wildMAC_wildIP(self): + """ IP6 MACIP wildcardMAC|wildIP ACL bridged traffic + """ + + self.run_traffic(self.WILD_MAC, self.WILD_IP, + self.BRIDGED, self.IS_IP6, 9) + + def test_acl_routed_ip4_exactMAC_exactIP(self): + """ IP4 MACIP exactMAC|exactIP ACL routed traffic + """ + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_exactMAC_exactIP(self): + """ IP6 MACIP exactMAC|exactIP ACL routed traffic + """ + + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_exactMAC_subnetIP(self): + """ IP4 MACIP exactMAC|subnetIP ACL routed traffic + """ + self.run_traffic(self.EXACT_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_exactMAC_subnetIP(self): + """ IP6 MACIP exactMAC|subnetIP ACL routed traffic + """ + + self.run_traffic(self.EXACT_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_exactMAC_wildIP(self): + """ IP4 MACIP exactMAC|wildIP ACL routed traffic + """ + self.run_traffic(self.EXACT_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_exactMAC_wildIP(self): + """ IP6 MACIP exactMAC|wildIP ACL routed traffic + """ + + self.run_traffic(self.EXACT_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_ouiMAC_exactIP(self): + """ IP4 MACIP ouiMAC|exactIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_ouiMAC_exactIP(self): + """ IP6 MACIP ouiMAC|exactIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_ouiMAC_subnetIP(self): + """ IP4 MACIP ouiMAC|subnetIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_ouiMAC_subnetIP(self): + """ IP6 MACIP ouiMAC|subnetIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_ouiMAC_wildIP(self): + """ IP4 MACIP ouiMAC|wildIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_ouiMAC_wildIP(self): + """ IP6 MACIP ouiMAC|wildIP ACL routed traffic + """ + + self.run_traffic(self.OUI_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_wildMAC_exactIP(self): + """ IP4 MACIP wildcardMAC|exactIP ACL routed traffic + """ + + self.run_traffic(self.WILD_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_wildMAC_exactIP(self): + """ IP6 MACIP wildcardMAC|exactIP ACL routed traffic + """ + + self.run_traffic(self.WILD_MAC, self.EXACT_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_routed_ip4_wildMAC_subnetIP(self): + """ IP4 MACIP wildcardMAC|subnetIP ACL routed traffic + """ + + self.run_traffic(self.WILD_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_wildMAC_subnetIP(self): + """ IP6 MACIP wildcardMAC|subnetIP ACL routed traffic + """ + + self.run_traffic(self.WILD_MAC, self.SUBNET_IP, + self.ROUTED, self.IS_IP6, 9) + + def test_acl_1_2(self): + """ MACIP ACL with 2 entries + """ + + self.run_test_acls(self.EXACT_MAC, self.WILD_IP, 1, [2]) + + def test_acl_1_5(self): + """ MACIP ACL with 5 entries + """ + + self.run_test_acls(self.EXACT_MAC, self.SUBNET_IP, 1, [5]) + + def test_acl_1_10(self): + """ MACIP ACL with 10 entries + """ + + self.run_test_acls(self.EXACT_MAC, self.EXACT_IP, 1, [10]) + + def test_acl_1_20(self): + """ MACIP ACL with 20 entries + """ + + self.run_test_acls(self.OUI_MAC, self.WILD_IP, 1, [20]) + + def test_acl_1_50(self): + """ MACIP ACL with 50 entries + """ + + self.run_test_acls(self.OUI_MAC, self.SUBNET_IP, 1, [50]) + + def test_acl_1_100(self): + """ MACIP ACL with 100 entries + """ + + self.run_test_acls(self.OUI_MAC, self.EXACT_IP, 1, [100]) + + def test_acl_2_X(self): + """ MACIP 2 ACLs each with 100+ entries + """ + + self.run_test_acls(self.OUI_MAC, self.SUBNET_IP, 2, [100, 200]) + + def test_acl_10_X(self): + """ MACIP 10 ACLs each with 100+ entries + """ + + self.run_test_acls(self.EXACT_MAC, self.EXACT_IP, 10, + [100, 120, 140, 160, 180, 200, 210, 220, 230, 240]) + + def test_acl_10_X_traffic_ip4(self): + """ MACIP 10 ACLs each with 100+ entries with IP4 traffic + """ + + self.run_test_acls(self.EXACT_MAC, self.EXACT_IP, 10, + [100, 120, 140, 160, 180, 200, 210, 220, 230, 240], + self.BRIDGED, self.IS_IP4) + + def test_acl_10_X_traffic_ip6(self): + """ MACIP 10 ACLs each with 100+ entries with IP6 traffic + """ + + self.run_test_acls(self.EXACT_MAC, self.EXACT_IP, 10, + [100, 120, 140, 160, 180, 200, 210, 220, 230, 240], + self.BRIDGED, self.IS_IP6) + + def test_acl_replace(self): + """ MACIP replace ACL + """ + + r1 = self.create_rules(acl_count=3, rules_count=[2, 2, 2]) + r2 = self.create_rules(mac_type=self.OUI_MAC, ip_type=self.SUBNET_IP) + self.apply_rules(r1) + + acls_before = self.macip_acl_dump_debug() + + # replace acls #2, #3 with new + reply = self.vapi.macip_acl_add_replace(r2[0], 2) + self.assertEqual(reply.retval, 0) + self.assertEqual(reply.acl_index, 2) + reply = self.vapi.macip_acl_add_replace(r2[1], 3) + self.assertEqual(reply.retval, 0) + self.assertEqual(reply.acl_index, 3) + + acls_after = self.macip_acl_dump_debug() + + # verify changes + self.assertEqual(len(acls_before), len(acls_after)) + for acl1, acl2 in zip( + acls_before[:2]+acls_before[4:], + acls_after[:2]+acls_after[4:]): + self.assertEqual(len(acl1), len(acl2)) + + self.assertEqual(len(acl1.r), len(acl2.r)) + for r1, r2 in zip(acl1.r, acl2.r): + self.assertEqual(len(acl1.r), len(acl2.r)) + self.assertEqual(acl1.r, acl2.r) + for acl1, acl2 in zip( + acls_before[2:4], + acls_after[2:4]): + self.assertEqual(len(acl1), len(acl2)) + + self.assertNotEqual(len(acl1.r), len(acl2.r)) + for r1, r2 in zip(acl1.r, acl2.r): + self.assertNotEqual(len(acl1.r), len(acl2.r)) + self.assertNotEqual(acl1.r, acl2.r) + + def test_acl_replace_traffic_ip4(self): + """ MACIP replace ACL with IP4 traffic + """ + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP4, 9) + + r = self.create_rules() + # replace acls #2, #3 with new + reply = self.vapi.macip_acl_add_replace(r[0], 0) + self.assertEqual(reply.retval, 0) + self.assertEqual(reply.acl_index, 0) + + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP4, 9, True) + + def test_acl_replace_traffic_ip6(self): + """ MACIP replace ACL with IP6 traffic + """ + self.run_traffic(self.OUI_MAC, self.SUBNET_IP, + self.BRIDGED, self.IS_IP6, 9) + + r = self.create_rules() + # replace acls #2, #3 with new + reply = self.vapi.macip_acl_add_replace(r[0], 0) + self.assertEqual(reply.retval, 0) + self.assertEqual(reply.acl_index, 0) + + self.run_traffic(self.EXACT_MAC, self.EXACT_IP, + self.BRIDGED, self.IS_IP6, 9, True) + + def test_delete_intf(self): + """ MACIP ACL delete intf with acl + """ + + intf_count = len(self.interfaces)+1 + intf = [] + self.apply_rules(self.create_rules(acl_count=3, rules_count=[3, 5, 4])) + + intf.append(VppLoInterface(self, 0)) + intf.append(VppLoInterface(self, 1)) + + sw_if_index0 = intf[0].sw_if_index + self.vapi.macip_acl_interface_add_del(sw_if_index0, 1) + + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, intf_count+1) + self.assertEqual(reply.acls[sw_if_index0], 1) + + sw_if_index1 = intf[1].sw_if_index + self.vapi.macip_acl_interface_add_del(sw_if_index1, 0) + + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, intf_count+2) + self.assertEqual(reply.acls[sw_if_index1], 0) + + intf[0].remove_vpp_config() + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, intf_count+2) + self.assertEqual(reply.acls[sw_if_index0], 4294967295) + self.assertEqual(reply.acls[sw_if_index1], 0) + + intf.append(VppLoInterface(self, 2)) + intf.append(VppLoInterface(self, 3)) + sw_if_index2 = intf[2].sw_if_index + sw_if_index3 = intf[3].sw_if_index + self.vapi.macip_acl_interface_add_del(sw_if_index2, 1) + self.vapi.macip_acl_interface_add_del(sw_if_index3, 1) + + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, intf_count+3) + self.assertEqual(reply.acls[sw_if_index1], 0) + self.assertEqual(reply.acls[sw_if_index2], 1) + self.assertEqual(reply.acls[sw_if_index3], 1) + + intf[2].remove_vpp_config() + intf[1].remove_vpp_config() + + reply = self.vapi.macip_acl_interface_get() + self.assertEqual(reply.count, intf_count+3) + self.assertEqual(reply.acls[sw_if_index0], 4294967295) + self.assertEqual(reply.acls[sw_if_index1], 4294967295) + self.assertEqual(reply.acls[sw_if_index2], 4294967295) + self.assertEqual(reply.acls[sw_if_index3], 1) + + intf[3].remove_vpp_config() + reply = self.vapi.macip_acl_interface_get() + + self.assertEqual(len([x for x in reply.acls if x != 4294967295]), 0) + + def test_acl_routed_ip4_wildMAC_wildIP(self): + """ IP4 MACIP wildcardMAC|wildIP ACL + """ + + self.run_traffic(self.WILD_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP4, 9) + + def test_acl_routed_ip6_wildMAC_wildIP(self): + """ IP6 MACIP wildcardMAC|wildIP ACL + """ + + self.run_traffic(self.WILD_MAC, self.WILD_IP, + self.ROUTED, self.IS_IP6, 9) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_bfd.py b/test/test_bfd.py new file mode 100644 index 00000000..4cb6d379 --- /dev/null +++ b/test/test_bfd.py @@ -0,0 +1,2591 @@ +#!/usr/bin/env python +""" BFD tests """ + +from __future__ import division +import unittest +import hashlib +import binascii +import time +from struct import pack, unpack +from random import randint, shuffle, getrandbits +from socket import AF_INET, AF_INET6, inet_ntop +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import UDP, IP +from scapy.layers.inet6 import IPv6 +from bfd import VppBFDAuthKey, BFD, BFDAuthType, VppBFDUDPSession, \ + BFDDiagCode, BFDState, BFD_vpp_echo +from framework import VppTestCase, VppTestRunner, running_extended_tests +from vpp_pg_interface import CaptureTimeoutError, is_ipv6_misc +from vpp_lo_interface import VppLoInterface +from util import ppp +from vpp_papi_provider import UnexpectedApiReturnValueError +from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto + +USEC_IN_SEC = 1000000 + + +class AuthKeyFactory(object): + """Factory class for creating auth keys with unique conf key ID""" + + def __init__(self): + self._conf_key_ids = {} + + def create_random_key(self, test, auth_type=BFDAuthType.keyed_sha1): + """ create a random key with unique conf key id """ + conf_key_id = randint(0, 0xFFFFFFFF) + while conf_key_id in self._conf_key_ids: + conf_key_id = randint(0, 0xFFFFFFFF) + self._conf_key_ids[conf_key_id] = 1 + key = str(bytearray([randint(0, 255) for _ in range(randint(1, 20))])) + return VppBFDAuthKey(test=test, auth_type=auth_type, + conf_key_id=conf_key_id, key=key) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDAPITestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) - API""" + + pg0 = None + pg1 = None + + @classmethod + def setUpClass(cls): + super(BFDAPITestCase, cls).setUpClass() + + try: + cls.create_pg_interfaces(range(2)) + for i in cls.pg_interfaces: + i.config_ip4() + i.config_ip6() + i.resolve_arp() + + except Exception: + super(BFDAPITestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFDAPITestCase, self).setUp() + self.factory = AuthKeyFactory() + + def test_add_bfd(self): + """ create a BFD session """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + + def test_double_add(self): + """ create the same BFD session twice (negative case) """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + + with self.vapi.expect_negative_api_retval(): + session.add_vpp_config() + + session.remove_vpp_config() + + def test_add_bfd6(self): + """ create IPv6 BFD session """ + session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip6, af=AF_INET6) + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + + def test_mod_bfd(self): + """ modify BFD session parameters """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + desired_min_tx=50000, + required_min_rx=10000, + detect_mult=1) + session.add_vpp_config() + s = session.get_bfd_udp_session_dump_entry() + self.assert_equal(session.desired_min_tx, + s.desired_min_tx, + "desired min transmit interval") + self.assert_equal(session.required_min_rx, + s.required_min_rx, + "required min receive interval") + self.assert_equal(session.detect_mult, s.detect_mult, "detect mult") + session.modify_parameters(desired_min_tx=session.desired_min_tx * 2, + required_min_rx=session.required_min_rx * 2, + detect_mult=session.detect_mult * 2) + s = session.get_bfd_udp_session_dump_entry() + self.assert_equal(session.desired_min_tx, + s.desired_min_tx, + "desired min transmit interval") + self.assert_equal(session.required_min_rx, + s.required_min_rx, + "required min receive interval") + self.assert_equal(session.detect_mult, s.detect_mult, "detect mult") + + def test_add_sha1_keys(self): + """ add SHA1 keys """ + key_count = 10 + keys = [self.factory.create_random_key( + self) for i in range(0, key_count)] + for key in keys: + self.assertFalse(key.query_vpp_config()) + for key in keys: + key.add_vpp_config() + for key in keys: + self.assertTrue(key.query_vpp_config()) + # remove randomly + indexes = range(key_count) + shuffle(indexes) + removed = [] + for i in indexes: + key = keys[i] + key.remove_vpp_config() + removed.append(i) + for j in range(key_count): + key = keys[j] + if j in removed: + self.assertFalse(key.query_vpp_config()) + else: + self.assertTrue(key.query_vpp_config()) + # should be removed now + for key in keys: + self.assertFalse(key.query_vpp_config()) + # add back and remove again + for key in keys: + key.add_vpp_config() + for key in keys: + self.assertTrue(key.query_vpp_config()) + for key in keys: + key.remove_vpp_config() + for key in keys: + self.assertFalse(key.query_vpp_config()) + + def test_add_bfd_sha1(self): + """ create a BFD session (SHA1) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key) + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + session.add_vpp_config() + self.logger.debug("Session state is %s", session.state) + session.remove_vpp_config() + + def test_double_add_sha1(self): + """ create the same BFD session twice (negative case) (SHA1) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key) + session.add_vpp_config() + with self.assertRaises(Exception): + session.add_vpp_config() + + def test_add_auth_nonexistent_key(self): + """ create BFD session using non-existent SHA1 (negative case) """ + session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, + sha1_key=self.factory.create_random_key(self)) + with self.assertRaises(Exception): + session.add_vpp_config() + + def test_shared_sha1_key(self): + """ share single SHA1 key between multiple BFD sessions """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + sessions = [ + VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key), + VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip6, + sha1_key=key, af=AF_INET6), + VppBFDUDPSession(self, self.pg1, self.pg1.remote_ip4, + sha1_key=key), + VppBFDUDPSession(self, self.pg1, self.pg1.remote_ip6, + sha1_key=key, af=AF_INET6)] + for s in sessions: + s.add_vpp_config() + removed = 0 + for s in sessions: + e = key.get_bfd_auth_keys_dump_entry() + self.assert_equal(e.use_count, len(sessions) - removed, + "Use count for shared key") + s.remove_vpp_config() + removed += 1 + e = key.get_bfd_auth_keys_dump_entry() + self.assert_equal(e.use_count, len(sessions) - removed, + "Use count for shared key") + + def test_activate_auth(self): + """ activate SHA1 authentication """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + session.activate_auth(key) + + def test_deactivate_auth(self): + """ deactivate SHA1 authentication """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + session.activate_auth(key) + session.deactivate_auth() + + def test_change_key(self): + """ change SHA1 key """ + key1 = self.factory.create_random_key(self) + key2 = self.factory.create_random_key(self) + while key2.conf_key_id == key1.conf_key_id: + key2 = self.factory.create_random_key(self) + key1.add_vpp_config() + key2.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key1) + session.add_vpp_config() + session.activate_auth(key2) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDTestSession(object): + """ BFD session as seen from test framework side """ + + def __init__(self, test, interface, af, detect_mult=3, sha1_key=None, + bfd_key_id=None, our_seq_number=None): + self.test = test + self.af = af + self.sha1_key = sha1_key + self.bfd_key_id = bfd_key_id + self.interface = interface + self.udp_sport = randint(49152, 65535) + if our_seq_number is None: + self.our_seq_number = randint(0, 40000000) + else: + self.our_seq_number = our_seq_number + self.vpp_seq_number = None + self.my_discriminator = 0 + self.desired_min_tx = 300000 + self.required_min_rx = 300000 + self.required_min_echo_rx = None + self.detect_mult = detect_mult + self.diag = BFDDiagCode.no_diagnostic + self.your_discriminator = None + self.state = BFDState.down + self.auth_type = BFDAuthType.no_auth + + def inc_seq_num(self): + """ increment sequence number, wrapping if needed """ + if self.our_seq_number == 0xFFFFFFFF: + self.our_seq_number = 0 + else: + self.our_seq_number += 1 + + def update(self, my_discriminator=None, your_discriminator=None, + desired_min_tx=None, required_min_rx=None, + required_min_echo_rx=None, detect_mult=None, + diag=None, state=None, auth_type=None): + """ update BFD parameters associated with session """ + if my_discriminator is not None: + self.my_discriminator = my_discriminator + if your_discriminator is not None: + self.your_discriminator = your_discriminator + if required_min_rx is not None: + self.required_min_rx = required_min_rx + if required_min_echo_rx is not None: + self.required_min_echo_rx = required_min_echo_rx + if desired_min_tx is not None: + self.desired_min_tx = desired_min_tx + if detect_mult is not None: + self.detect_mult = detect_mult + if diag is not None: + self.diag = diag + if state is not None: + self.state = state + if auth_type is not None: + self.auth_type = auth_type + + def fill_packet_fields(self, packet): + """ set packet fields with known values in packet """ + bfd = packet[BFD] + if self.my_discriminator: + self.test.logger.debug("BFD: setting packet.my_discriminator=%s", + self.my_discriminator) + bfd.my_discriminator = self.my_discriminator + if self.your_discriminator: + self.test.logger.debug("BFD: setting packet.your_discriminator=%s", + self.your_discriminator) + bfd.your_discriminator = self.your_discriminator + if self.required_min_rx: + self.test.logger.debug( + "BFD: setting packet.required_min_rx_interval=%s", + self.required_min_rx) + bfd.required_min_rx_interval = self.required_min_rx + if self.required_min_echo_rx: + self.test.logger.debug( + "BFD: setting packet.required_min_echo_rx=%s", + self.required_min_echo_rx) + bfd.required_min_echo_rx_interval = self.required_min_echo_rx + if self.desired_min_tx: + self.test.logger.debug( + "BFD: setting packet.desired_min_tx_interval=%s", + self.desired_min_tx) + bfd.desired_min_tx_interval = self.desired_min_tx + if self.detect_mult: + self.test.logger.debug( + "BFD: setting packet.detect_mult=%s", self.detect_mult) + bfd.detect_mult = self.detect_mult + if self.diag: + self.test.logger.debug("BFD: setting packet.diag=%s", self.diag) + bfd.diag = self.diag + if self.state: + self.test.logger.debug("BFD: setting packet.state=%s", self.state) + bfd.state = self.state + if self.auth_type: + # this is used by a negative test-case + self.test.logger.debug("BFD: setting packet.auth_type=%s", + self.auth_type) + bfd.auth_type = self.auth_type + + def create_packet(self): + """ create a BFD packet, reflecting the current state of session """ + if self.sha1_key: + bfd = BFD(flags="A") + bfd.auth_type = self.sha1_key.auth_type + bfd.auth_len = BFD.sha1_auth_len + bfd.auth_key_id = self.bfd_key_id + bfd.auth_seq_num = self.our_seq_number + bfd.length = BFD.sha1_auth_len + BFD.bfd_pkt_len + else: + bfd = BFD() + if self.af == AF_INET6: + packet = (Ether(src=self.interface.remote_mac, + dst=self.interface.local_mac) / + IPv6(src=self.interface.remote_ip6, + dst=self.interface.local_ip6, + hlim=255) / + UDP(sport=self.udp_sport, dport=BFD.udp_dport) / + bfd) + else: + packet = (Ether(src=self.interface.remote_mac, + dst=self.interface.local_mac) / + IP(src=self.interface.remote_ip4, + dst=self.interface.local_ip4, + ttl=255) / + UDP(sport=self.udp_sport, dport=BFD.udp_dport) / + bfd) + self.test.logger.debug("BFD: Creating packet") + self.fill_packet_fields(packet) + if self.sha1_key: + hash_material = str(packet[BFD])[:32] + self.sha1_key.key + \ + "\0" * (20 - len(self.sha1_key.key)) + self.test.logger.debug("BFD: Calculated SHA1 hash: %s" % + hashlib.sha1(hash_material).hexdigest()) + packet[BFD].auth_key_hash = hashlib.sha1(hash_material).digest() + return packet + + def send_packet(self, packet=None, interface=None): + """ send packet on interface, creating the packet if needed """ + if packet is None: + packet = self.create_packet() + if interface is None: + interface = self.test.pg0 + self.test.logger.debug(ppp("Sending packet:", packet)) + interface.add_stream(packet) + self.test.pg_start() + + def verify_sha1_auth(self, packet): + """ Verify correctness of authentication in BFD layer. """ + bfd = packet[BFD] + self.test.assert_equal(bfd.auth_len, 28, "Auth section length") + self.test.assert_equal(bfd.auth_type, self.sha1_key.auth_type, + BFDAuthType) + self.test.assert_equal(bfd.auth_key_id, self.bfd_key_id, "Key ID") + self.test.assert_equal(bfd.auth_reserved, 0, "Reserved") + if self.vpp_seq_number is None: + self.vpp_seq_number = bfd.auth_seq_num + self.test.logger.debug("Received initial sequence number: %s" % + self.vpp_seq_number) + else: + recvd_seq_num = bfd.auth_seq_num + self.test.logger.debug("Received followup sequence number: %s" % + recvd_seq_num) + if self.vpp_seq_number < 0xffffffff: + if self.sha1_key.auth_type == \ + BFDAuthType.meticulous_keyed_sha1: + self.test.assert_equal(recvd_seq_num, + self.vpp_seq_number + 1, + "BFD sequence number") + else: + self.test.assert_in_range(recvd_seq_num, + self.vpp_seq_number, + self.vpp_seq_number + 1, + "BFD sequence number") + else: + if self.sha1_key.auth_type == \ + BFDAuthType.meticulous_keyed_sha1: + self.test.assert_equal(recvd_seq_num, 0, + "BFD sequence number") + else: + self.test.assertIn(recvd_seq_num, (self.vpp_seq_number, 0), + "BFD sequence number not one of " + "(%s, 0)" % self.vpp_seq_number) + self.vpp_seq_number = recvd_seq_num + # last 20 bytes represent the hash - so replace them with the key, + # pad the result with zeros and hash the result + hash_material = bfd.original[:-20] + self.sha1_key.key + \ + "\0" * (20 - len(self.sha1_key.key)) + expected_hash = hashlib.sha1(hash_material).hexdigest() + self.test.assert_equal(binascii.hexlify(bfd.auth_key_hash), + expected_hash, "Auth key hash") + + def verify_bfd(self, packet): + """ Verify correctness of BFD layer. """ + bfd = packet[BFD] + self.test.assert_equal(bfd.version, 1, "BFD version") + self.test.assert_equal(bfd.your_discriminator, + self.my_discriminator, + "BFD - your discriminator") + if self.sha1_key: + self.verify_sha1_auth(packet) + + +def bfd_session_up(test): + """ Bring BFD session up """ + test.logger.info("BFD: Waiting for slow hello") + p = wait_for_bfd_packet(test, 2) + old_offset = None + if hasattr(test, 'vpp_clock_offset'): + old_offset = test.vpp_clock_offset + test.vpp_clock_offset = time.time() - p.time + test.logger.debug("BFD: Calculated vpp clock offset: %s", + test.vpp_clock_offset) + if old_offset: + test.assertAlmostEqual( + old_offset, test.vpp_clock_offset, delta=0.5, + msg="vpp clock offset not stable (new: %s, old: %s)" % + (test.vpp_clock_offset, old_offset)) + test.logger.info("BFD: Sending Init") + test.test_session.update(my_discriminator=randint(0, 40000000), + your_discriminator=p[BFD].my_discriminator, + state=BFDState.init) + if test.test_session.sha1_key and test.test_session.sha1_key.auth_type == \ + BFDAuthType.meticulous_keyed_sha1: + test.test_session.inc_seq_num() + test.test_session.send_packet() + test.logger.info("BFD: Waiting for event") + e = test.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(test, e, expected_state=BFDState.up) + test.logger.info("BFD: Session is Up") + test.test_session.update(state=BFDState.up) + if test.test_session.sha1_key and test.test_session.sha1_key.auth_type == \ + BFDAuthType.meticulous_keyed_sha1: + test.test_session.inc_seq_num() + test.test_session.send_packet() + test.assert_equal(test.vpp_session.state, BFDState.up, BFDState) + + +def bfd_session_down(test): + """ Bring BFD session down """ + test.assert_equal(test.vpp_session.state, BFDState.up, BFDState) + test.test_session.update(state=BFDState.down) + if test.test_session.sha1_key and test.test_session.sha1_key.auth_type == \ + BFDAuthType.meticulous_keyed_sha1: + test.test_session.inc_seq_num() + test.test_session.send_packet() + test.logger.info("BFD: Waiting for event") + e = test.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(test, e, expected_state=BFDState.down) + test.logger.info("BFD: Session is Down") + test.assert_equal(test.vpp_session.state, BFDState.down, BFDState) + + +def verify_bfd_session_config(test, session, state=None): + dump = session.get_bfd_udp_session_dump_entry() + test.assertIsNotNone(dump) + # since dump is not none, we have verified that sw_if_index and addresses + # are valid (in get_bfd_udp_session_dump_entry) + if state: + test.assert_equal(dump.state, state, "session state") + test.assert_equal(dump.required_min_rx, session.required_min_rx, + "required min rx interval") + test.assert_equal(dump.desired_min_tx, session.desired_min_tx, + "desired min tx interval") + test.assert_equal(dump.detect_mult, session.detect_mult, + "detect multiplier") + if session.sha1_key is None: + test.assert_equal(dump.is_authenticated, 0, "is_authenticated flag") + else: + test.assert_equal(dump.is_authenticated, 1, "is_authenticated flag") + test.assert_equal(dump.bfd_key_id, session.bfd_key_id, + "bfd key id") + test.assert_equal(dump.conf_key_id, + session.sha1_key.conf_key_id, + "config key id") + + +def verify_ip(test, packet): + """ Verify correctness of IP layer. """ + if test.vpp_session.af == AF_INET6: + ip = packet[IPv6] + local_ip = test.pg0.local_ip6 + remote_ip = test.pg0.remote_ip6 + test.assert_equal(ip.hlim, 255, "IPv6 hop limit") + else: + ip = packet[IP] + local_ip = test.pg0.local_ip4 + remote_ip = test.pg0.remote_ip4 + test.assert_equal(ip.ttl, 255, "IPv4 TTL") + test.assert_equal(ip.src, local_ip, "IP source address") + test.assert_equal(ip.dst, remote_ip, "IP destination address") + + +def verify_udp(test, packet): + """ Verify correctness of UDP layer. """ + udp = packet[UDP] + test.assert_equal(udp.dport, BFD.udp_dport, "UDP destination port") + test.assert_in_range(udp.sport, BFD.udp_sport_min, BFD.udp_sport_max, + "UDP source port") + + +def verify_event(test, event, expected_state): + """ Verify correctness of event values. """ + e = event + test.logger.debug("BFD: Event: %s" % repr(e)) + test.assert_equal(e.sw_if_index, + test.vpp_session.interface.sw_if_index, + "BFD interface index") + is_ipv6 = 0 + if test.vpp_session.af == AF_INET6: + is_ipv6 = 1 + test.assert_equal(e.is_ipv6, is_ipv6, "is_ipv6") + if test.vpp_session.af == AF_INET: + test.assert_equal(e.local_addr[:4], test.vpp_session.local_addr_n, + "Local IPv4 address") + test.assert_equal(e.peer_addr[:4], test.vpp_session.peer_addr_n, + "Peer IPv4 address") + else: + test.assert_equal(e.local_addr, test.vpp_session.local_addr_n, + "Local IPv6 address") + test.assert_equal(e.peer_addr, test.vpp_session.peer_addr_n, + "Peer IPv6 address") + test.assert_equal(e.state, expected_state, BFDState) + + +def wait_for_bfd_packet(test, timeout=1, pcap_time_min=None): + """ wait for BFD packet and verify its correctness + + :param timeout: how long to wait + :param pcap_time_min: ignore packets with pcap timestamp lower than this + + :returns: tuple (packet, time spent waiting for packet) + """ + test.logger.info("BFD: Waiting for BFD packet") + deadline = time.time() + timeout + counter = 0 + while True: + counter += 1 + # sanity check + test.assert_in_range(counter, 0, 100, "number of packets ignored") + time_left = deadline - time.time() + if time_left < 0: + raise CaptureTimeoutError("Packet did not arrive within timeout") + p = test.pg0.wait_for_packet(timeout=time_left) + test.logger.debug(ppp("BFD: Got packet:", p)) + if pcap_time_min is not None and p.time < pcap_time_min: + test.logger.debug(ppp("BFD: ignoring packet (pcap time %s < " + "pcap time min %s):" % + (p.time, pcap_time_min), p)) + else: + break + bfd = p[BFD] + if bfd is None: + raise Exception(ppp("Unexpected or invalid BFD packet:", p)) + if bfd.payload: + raise Exception(ppp("Unexpected payload in BFD packet:", bfd)) + verify_ip(test, p) + verify_udp(test, p) + test.test_session.verify_bfd(p) + return p + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFD4TestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD)""" + + pg0 = None + vpp_clock_offset = None + vpp_session = None + test_session = None + + @classmethod + def setUpClass(cls): + super(BFD4TestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.create_loopback_interfaces([0]) + cls.loopback0 = cls.lo_interfaces[0] + cls.loopback0.config_ip4() + cls.loopback0.admin_up() + cls.pg0.config_ip4() + cls.pg0.configure_ipv4_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFD4TestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFD4TestCase, self).setUp() + self.factory = AuthKeyFactory() + self.vapi.want_bfd_events() + self.pg0.enable_capture() + try: + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0, AF_INET) + except: + self.vapi.want_bfd_events(enable_disable=0) + raise + + def tearDown(self): + if not self.vpp_dead: + self.vapi.want_bfd_events(enable_disable=0) + self.vapi.collect_events() # clear the event queue + super(BFD4TestCase, self).tearDown() + + def test_session_up(self): + """ bring BFD session up """ + bfd_session_up(self) + + def test_session_up_by_ip(self): + """ bring BFD session up - first frame looked up by address pair """ + self.logger.info("BFD: Sending Slow control frame") + self.test_session.update(my_discriminator=randint(0, 40000000)) + self.test_session.send_packet() + self.pg0.enable_capture() + p = self.pg0.wait_for_packet(1) + self.assert_equal(p[BFD].your_discriminator, + self.test_session.my_discriminator, + "BFD - your discriminator") + self.assert_equal(p[BFD].state, BFDState.init, BFDState) + self.test_session.update(your_discriminator=p[BFD].my_discriminator, + state=BFDState.up) + self.logger.info("BFD: Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.init) + self.logger.info("BFD: Sending Up") + self.test_session.send_packet() + self.logger.info("BFD: Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.up) + self.logger.info("BFD: Session is Up") + self.test_session.update(state=BFDState.up) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + def test_session_down(self): + """ bring BFD session down """ + bfd_session_up(self) + bfd_session_down(self) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_hold_up(self): + """ hold BFD session up """ + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + wait_for_bfd_packet(self) + self.test_session.send_packet() + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_slow_timer(self): + """ verify slow periodic control frames while session down """ + packet_count = 3 + self.logger.info("BFD: Waiting for %d BFD packets", packet_count) + prev_packet = wait_for_bfd_packet(self, 2) + for dummy in range(packet_count): + next_packet = wait_for_bfd_packet(self, 2) + time_diff = next_packet.time - prev_packet.time + # spec says the range should be <0.75, 1>, allow extra 0.05 margin + # to work around timing issues + self.assert_in_range( + time_diff, 0.70, 1.05, "time between slow packets") + prev_packet = next_packet + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_zero_remote_min_rx(self): + """ no packets when zero remote required min rx interval """ + bfd_session_up(self) + self.test_session.update(required_min_rx=0) + self.test_session.send_packet() + for dummy in range(self.test_session.detect_mult): + self.sleep(self.vpp_session.required_min_rx / USEC_IN_SEC, + "sleep before transmitting bfd packet") + self.test_session.send_packet() + try: + p = wait_for_bfd_packet(self, timeout=0) + self.logger.error(ppp("Received unexpected packet:", p)) + except CaptureTimeoutError: + pass + self.assert_equal( + len(self.vapi.collect_events()), 0, "number of bfd events") + self.test_session.update(required_min_rx=300000) + for dummy in range(3): + self.test_session.send_packet() + wait_for_bfd_packet( + self, timeout=self.test_session.required_min_rx / USEC_IN_SEC) + self.assert_equal( + len(self.vapi.collect_events()), 0, "number of bfd events") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_conn_down(self): + """ verify session goes down after inactivity """ + bfd_session_up(self) + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + self.sleep(detection_time, "waiting for BFD session time-out") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.down) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_large_required_min_rx(self): + """ large remote required min rx interval """ + bfd_session_up(self) + p = wait_for_bfd_packet(self) + interval = 3000000 + self.test_session.update(required_min_rx=interval) + self.test_session.send_packet() + time_mark = time.time() + count = 0 + # busy wait here, trying to collect a packet or event, vpp is not + # allowed to send packets and the session will timeout first - so the + # Up->Down event must arrive before any packets do + while time.time() < time_mark + interval / USEC_IN_SEC: + try: + p = wait_for_bfd_packet(self, timeout=0) + # if vpp managed to send a packet before we did the session + # session update, then that's fine, ignore it + if p.time < time_mark - self.vpp_clock_offset: + continue + self.logger.error(ppp("Received unexpected packet:", p)) + count += 1 + except CaptureTimeoutError: + pass + events = self.vapi.collect_events() + if len(events) > 0: + verify_event(self, events[0], BFDState.down) + break + self.assert_equal(count, 0, "number of packets received") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_immediate_remote_min_rx_reduction(self): + """ immediately honor remote required min rx reduction """ + self.vpp_session.remove_vpp_config() + self.vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, desired_min_tx=10000) + self.pg0.enable_capture() + self.vpp_session.add_vpp_config() + self.test_session.update(desired_min_tx=1000000, + required_min_rx=1000000) + bfd_session_up(self) + reference_packet = wait_for_bfd_packet(self) + time_mark = time.time() + interval = 300000 + self.test_session.update(required_min_rx=interval) + self.test_session.send_packet() + extra_time = time.time() - time_mark + p = wait_for_bfd_packet(self) + # first packet is allowed to be late by time we spent doing the update + # calculated in extra_time + self.assert_in_range(p.time - reference_packet.time, + .95 * 0.75 * interval / USEC_IN_SEC, + 1.05 * interval / USEC_IN_SEC + extra_time, + "time between BFD packets") + reference_packet = p + for dummy in range(3): + p = wait_for_bfd_packet(self) + diff = p.time - reference_packet.time + self.assert_in_range(diff, .95 * .75 * interval / USEC_IN_SEC, + 1.05 * interval / USEC_IN_SEC, + "time between BFD packets") + reference_packet = p + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_modify_req_min_rx_double(self): + """ modify session - double required min rx """ + bfd_session_up(self) + p = wait_for_bfd_packet(self) + self.test_session.update(desired_min_tx=10000, + required_min_rx=10000) + self.test_session.send_packet() + # double required min rx + self.vpp_session.modify_parameters( + required_min_rx=2 * self.vpp_session.required_min_rx) + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + # poll bit needs to be set + self.assertIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + # finish poll sequence with final packet + final = self.test_session.create_packet() + final[BFD].flags = "F" + timeout = self.test_session.detect_mult * \ + max(self.test_session.desired_min_tx, + self.vpp_session.required_min_rx) / USEC_IN_SEC + self.test_session.send_packet(final) + time_mark = time.time() + e = self.vapi.wait_for_event(2 * timeout, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.down) + time_to_event = time.time() - time_mark + self.assert_in_range(time_to_event, .9 * timeout, + 1.1 * timeout, "session timeout") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_modify_req_min_rx_halve(self): + """ modify session - halve required min rx """ + self.vpp_session.modify_parameters( + required_min_rx=2 * self.vpp_session.required_min_rx) + bfd_session_up(self) + p = wait_for_bfd_packet(self) + self.test_session.update(desired_min_tx=10000, + required_min_rx=10000) + self.test_session.send_packet() + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + # halve required min rx + old_required_min_rx = self.vpp_session.required_min_rx + self.vpp_session.modify_parameters( + required_min_rx=0.5 * self.vpp_session.required_min_rx) + # now we wait 0.8*3*old-req-min-rx and the session should still be up + self.sleep(0.8 * self.vpp_session.detect_mult * + old_required_min_rx / USEC_IN_SEC, + "wait before finishing poll sequence") + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + p = wait_for_bfd_packet(self) + # poll bit needs to be set + self.assertIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + # finish poll sequence with final packet + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + # now the session should time out under new conditions + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + before = time.time() + e = self.vapi.wait_for_event( + 2 * detection_time, "bfd_udp_session_details") + after = time.time() + self.assert_in_range(after - before, + 0.9 * detection_time, + 1.1 * detection_time, + "time before bfd session goes down") + verify_event(self, e, expected_state=BFDState.down) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_modify_detect_mult(self): + """ modify detect multiplier """ + bfd_session_up(self) + p = wait_for_bfd_packet(self) + self.vpp_session.modify_parameters(detect_mult=1) + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(self.vpp_session.detect_mult, + p[BFD].detect_mult, + "detect mult") + # poll bit must not be set + self.assertNotIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + self.vpp_session.modify_parameters(detect_mult=10) + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(self.vpp_session.detect_mult, + p[BFD].detect_mult, + "detect mult") + # poll bit must not be set + self.assertNotIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_queued_poll(self): + """ test poll sequence queueing """ + bfd_session_up(self) + p = wait_for_bfd_packet(self) + self.vpp_session.modify_parameters( + required_min_rx=2 * self.vpp_session.required_min_rx) + p = wait_for_bfd_packet(self) + poll_sequence_start = time.time() + poll_sequence_length_min = 0.5 + send_final_after = time.time() + poll_sequence_length_min + # poll bit needs to be set + self.assertIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + self.assert_equal(p[BFD].required_min_rx_interval, + self.vpp_session.required_min_rx, + "BFD required min rx interval") + self.vpp_session.modify_parameters( + required_min_rx=2 * self.vpp_session.required_min_rx) + # 2nd poll sequence should be queued now + # don't send the reply back yet, wait for some time to emulate + # longer round-trip time + packet_count = 0 + while time.time() < send_final_after: + self.test_session.send_packet() + p = wait_for_bfd_packet(self) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + self.assert_equal(p[BFD].required_min_rx_interval, + self.vpp_session.required_min_rx, + "BFD required min rx interval") + packet_count += 1 + # poll bit must be set + self.assertIn("P", p.sprintf("%BFD.flags%"), + "Poll bit not set in BFD packet") + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + # finish 1st with final + poll_sequence_length = time.time() - poll_sequence_start + # vpp must wait for some time before starting new poll sequence + poll_no_2_started = False + for dummy in range(2 * packet_count): + p = wait_for_bfd_packet(self) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + if "P" in p.sprintf("%BFD.flags%"): + poll_no_2_started = True + if time.time() < poll_sequence_start + poll_sequence_length: + raise Exception("VPP started 2nd poll sequence too soon") + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + break + else: + self.test_session.send_packet() + self.assertTrue(poll_no_2_started, "2nd poll sequence not performed") + # finish 2nd with final + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + p = wait_for_bfd_packet(self) + # poll bit must not be set + self.assertNotIn("P", p.sprintf("%BFD.flags%"), + "Poll bit set in BFD packet") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_poll_response(self): + """ test correct response to control frame with poll bit set """ + bfd_session_up(self) + poll = self.test_session.create_packet() + poll[BFD].flags = "P" + self.test_session.send_packet(poll) + final = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assertIn("F", final.sprintf("%BFD.flags%")) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_no_periodic_if_remote_demand(self): + """ no periodic frames outside poll sequence if remote demand set """ + bfd_session_up(self) + demand = self.test_session.create_packet() + demand[BFD].flags = "D" + self.test_session.send_packet(demand) + transmit_time = 0.9 \ + * max(self.vpp_session.required_min_rx, + self.test_session.desired_min_tx) \ + / USEC_IN_SEC + count = 0 + for dummy in range(self.test_session.detect_mult * 2): + time.sleep(transmit_time) + self.test_session.send_packet(demand) + try: + p = wait_for_bfd_packet(self, timeout=0) + self.logger.error(ppp("Received unexpected packet:", p)) + count += 1 + except CaptureTimeoutError: + pass + events = self.vapi.collect_events() + for e in events: + self.logger.error("Received unexpected event: %s", e) + self.assert_equal(count, 0, "number of packets received") + self.assert_equal(len(events), 0, "number of events received") + + def test_echo_looped_back(self): + """ echo packets looped back """ + # don't need a session in this case.. + self.vpp_session.remove_vpp_config() + self.pg0.enable_capture() + echo_packet_count = 10 + # random source port low enough to increment a few times.. + udp_sport_tx = randint(1, 50000) + udp_sport_rx = udp_sport_tx + echo_packet = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg0.remote_ip4) / + UDP(dport=BFD.udp_dport_echo) / + Raw("this should be looped back")) + for dummy in range(echo_packet_count): + self.sleep(.01, "delay between echo packets") + echo_packet[UDP].sport = udp_sport_tx + udp_sport_tx += 1 + self.logger.debug(ppp("Sending packet:", echo_packet)) + self.pg0.add_stream(echo_packet) + self.pg_start() + for dummy in range(echo_packet_count): + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + ether = p[Ether] + self.assert_equal(self.pg0.remote_mac, + ether.dst, "Destination MAC") + self.assert_equal(self.pg0.local_mac, ether.src, "Source MAC") + ip = p[IP] + self.assert_equal(self.pg0.remote_ip4, ip.dst, "Destination IP") + self.assert_equal(self.pg0.remote_ip4, ip.src, "Destination IP") + udp = p[UDP] + self.assert_equal(udp.dport, BFD.udp_dport_echo, + "UDP destination port") + self.assert_equal(udp.sport, udp_sport_rx, "UDP source port") + udp_sport_rx += 1 + # need to compare the hex payload here, otherwise BFD_vpp_echo + # gets in way + self.assertEqual(str(p[UDP].payload), + str(echo_packet[UDP].payload), + "Received packet is not the echo packet sent") + self.assert_equal(udp_sport_tx, udp_sport_rx, "UDP source port (== " + "ECHO packet identifier for test purposes)") + + def test_echo(self): + """ echo function """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.test_session.send_packet() + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + # echo shouldn't work without echo source set + for dummy in range(10): + sleep = self.vpp_session.required_min_rx / USEC_IN_SEC + self.sleep(sleep, "delay before sending bfd packet") + self.test_session.send_packet() + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(p[BFD].required_min_rx_interval, + self.vpp_session.required_min_rx, + "BFD required min rx interval") + self.test_session.send_packet() + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + echo_seen = False + # should be turned on - loopback echo packets + for dummy in range(3): + loop_until = time.time() + 0.75 * detection_time + while time.time() < loop_until: + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + if p[UDP].dport == BFD.udp_dport_echo: + self.assert_equal( + p[IP].dst, self.pg0.local_ip4, "BFD ECHO dst IP") + self.assertNotEqual(p[IP].src, self.loopback0.local_ip4, + "BFD ECHO src IP equal to loopback IP") + self.logger.debug(ppp("Looping back packet:", p)) + self.assert_equal(p[Ether].dst, self.pg0.remote_mac, + "ECHO packet destination MAC address") + p[Ether].dst = self.pg0.local_mac + self.pg0.add_stream(p) + self.pg_start() + echo_seen = True + elif p.haslayer(BFD): + if echo_seen: + self.assertGreaterEqual( + p[BFD].required_min_rx_interval, + 1000000) + if "P" in p.sprintf("%BFD.flags%"): + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + else: + raise Exception(ppp("Received unknown packet:", p)) + + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + self.test_session.send_packet() + self.assertTrue(echo_seen, "No echo packets received") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_echo_fail(self): + """ session goes down if echo function fails """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.test_session.send_packet() + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + # echo function should be used now, but we will drop the echo packets + verified_diag = False + for dummy in range(3): + loop_until = time.time() + 0.75 * detection_time + while time.time() < loop_until: + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + if p[UDP].dport == BFD.udp_dport_echo: + # dropped + pass + elif p.haslayer(BFD): + if "P" in p.sprintf("%BFD.flags%"): + self.assertGreaterEqual( + p[BFD].required_min_rx_interval, + 1000000) + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + if p[BFD].state == BFDState.down: + self.assert_equal(p[BFD].diag, + BFDDiagCode.echo_function_failed, + BFDDiagCode) + verified_diag = True + else: + raise Exception(ppp("Received unknown packet:", p)) + self.test_session.send_packet() + events = self.vapi.collect_events() + self.assert_equal(len(events), 1, "number of bfd events") + self.assert_equal(events[0].state, BFDState.down, BFDState) + self.assertTrue(verified_diag, "Incorrect diagnostics code received") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_echo_stop(self): + """ echo function stops if peer sets required min echo rx zero """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.test_session.send_packet() + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + # wait for first echo packet + while True: + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + if p[UDP].dport == BFD.udp_dport_echo: + self.logger.debug(ppp("Looping back packet:", p)) + p[Ether].dst = self.pg0.local_mac + self.pg0.add_stream(p) + self.pg_start() + break + elif p.haslayer(BFD): + # ignore BFD + pass + else: + raise Exception(ppp("Received unknown packet:", p)) + self.test_session.update(required_min_echo_rx=0) + self.test_session.send_packet() + # echo packets shouldn't arrive anymore + for dummy in range(5): + wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.test_session.send_packet() + events = self.vapi.collect_events() + self.assert_equal(len(events), 0, "number of bfd events") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_echo_source_removed(self): + """ echo function stops if echo source is removed """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.test_session.send_packet() + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + # wait for first echo packet + while True: + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + if p[UDP].dport == BFD.udp_dport_echo: + self.logger.debug(ppp("Looping back packet:", p)) + p[Ether].dst = self.pg0.local_mac + self.pg0.add_stream(p) + self.pg_start() + break + elif p.haslayer(BFD): + # ignore BFD + pass + else: + raise Exception(ppp("Received unknown packet:", p)) + self.vapi.bfd_udp_del_echo_source() + self.test_session.send_packet() + # echo packets shouldn't arrive anymore + for dummy in range(5): + wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.test_session.send_packet() + events = self.vapi.collect_events() + self.assert_equal(len(events), 0, "number of bfd events") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_stale_echo(self): + """ stale echo packets don't keep a session up """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + self.test_session.send_packet() + # should be turned on - loopback echo packets + echo_packet = None + timeout_at = None + timeout_ok = False + for dummy in range(10 * self.vpp_session.detect_mult): + p = self.pg0.wait_for_packet(1) + if p[UDP].dport == BFD.udp_dport_echo: + if echo_packet is None: + self.logger.debug(ppp("Got first echo packet:", p)) + echo_packet = p + timeout_at = time.time() + self.vpp_session.detect_mult * \ + self.test_session.required_min_echo_rx / USEC_IN_SEC + else: + self.logger.debug(ppp("Got followup echo packet:", p)) + self.logger.debug(ppp("Looping back first echo packet:", p)) + echo_packet[Ether].dst = self.pg0.local_mac + self.pg0.add_stream(echo_packet) + self.pg_start() + elif p.haslayer(BFD): + self.logger.debug(ppp("Got packet:", p)) + if "P" in p.sprintf("%BFD.flags%"): + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + if p[BFD].state == BFDState.down: + self.assertIsNotNone( + timeout_at, + "Session went down before first echo packet received") + now = time.time() + self.assertGreaterEqual( + now, timeout_at, + "Session timeout at %s, but is expected at %s" % + (now, timeout_at)) + self.assert_equal(p[BFD].diag, + BFDDiagCode.echo_function_failed, + BFDDiagCode) + events = self.vapi.collect_events() + self.assert_equal(len(events), 1, "number of bfd events") + self.assert_equal(events[0].state, BFDState.down, BFDState) + timeout_ok = True + break + else: + raise Exception(ppp("Received unknown packet:", p)) + self.test_session.send_packet() + self.assertTrue(timeout_ok, "Expected timeout event didn't occur") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_invalid_echo_checksum(self): + """ echo packets with invalid checksum don't keep a session up """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + self.test_session.send_packet() + # should be turned on - loopback echo packets + timeout_at = None + timeout_ok = False + for dummy in range(10 * self.vpp_session.detect_mult): + p = self.pg0.wait_for_packet(1) + if p[UDP].dport == BFD.udp_dport_echo: + self.logger.debug(ppp("Got echo packet:", p)) + if timeout_at is None: + timeout_at = time.time() + self.vpp_session.detect_mult * \ + self.test_session.required_min_echo_rx / USEC_IN_SEC + p[BFD_vpp_echo].checksum = getrandbits(64) + p[Ether].dst = self.pg0.local_mac + self.logger.debug(ppp("Looping back modified echo packet:", p)) + self.pg0.add_stream(p) + self.pg_start() + elif p.haslayer(BFD): + self.logger.debug(ppp("Got packet:", p)) + if "P" in p.sprintf("%BFD.flags%"): + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + if p[BFD].state == BFDState.down: + self.assertIsNotNone( + timeout_at, + "Session went down before first echo packet received") + now = time.time() + self.assertGreaterEqual( + now, timeout_at, + "Session timeout at %s, but is expected at %s" % + (now, timeout_at)) + self.assert_equal(p[BFD].diag, + BFDDiagCode.echo_function_failed, + BFDDiagCode) + events = self.vapi.collect_events() + self.assert_equal(len(events), 1, "number of bfd events") + self.assert_equal(events[0].state, BFDState.down, BFDState) + timeout_ok = True + break + else: + raise Exception(ppp("Received unknown packet:", p)) + self.test_session.send_packet() + self.assertTrue(timeout_ok, "Expected timeout event didn't occur") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_admin_up_down(self): + """ put session admin-up and admin-down """ + bfd_session_up(self) + self.vpp_session.admin_down() + self.pg0.enable_capture() + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.admin_down) + for dummy in range(2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.admin_down, BFDState) + # try to bring session up - shouldn't be possible + self.test_session.update(state=BFDState.init) + self.test_session.send_packet() + for dummy in range(2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.admin_down, BFDState) + self.vpp_session.admin_up() + self.test_session.update(state=BFDState.down) + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.down) + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(p[BFD].state, BFDState.down, BFDState) + self.test_session.send_packet() + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(p[BFD].state, BFDState.init, BFDState) + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.init) + self.test_session.update(state=BFDState.up) + self.test_session.send_packet() + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.up) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_config_change_remote_demand(self): + """ configuration change while peer in demand mode """ + bfd_session_up(self) + demand = self.test_session.create_packet() + demand[BFD].flags = "D" + self.test_session.send_packet(demand) + self.vpp_session.modify_parameters( + required_min_rx=2 * self.vpp_session.required_min_rx) + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + # poll bit must be set + self.assertIn("P", p.sprintf("%BFD.flags%"), "Poll bit not set") + # terminate poll sequence + final = self.test_session.create_packet() + final[BFD].flags = "D+F" + self.test_session.send_packet(final) + # vpp should be quiet now again + transmit_time = 0.9 \ + * max(self.vpp_session.required_min_rx, + self.test_session.desired_min_tx) \ + / USEC_IN_SEC + count = 0 + for dummy in range(self.test_session.detect_mult * 2): + time.sleep(transmit_time) + self.test_session.send_packet(demand) + try: + p = wait_for_bfd_packet(self, timeout=0) + self.logger.error(ppp("Received unexpected packet:", p)) + count += 1 + except CaptureTimeoutError: + pass + events = self.vapi.collect_events() + for e in events: + self.logger.error("Received unexpected event: %s", e) + self.assert_equal(count, 0, "number of packets received") + self.assert_equal(len(events), 0, "number of events received") + + def test_intf_deleted(self): + """ interface with bfd session deleted """ + intf = VppLoInterface(self, 0) + intf.config_ip4() + intf.admin_up() + sw_if_index = intf.sw_if_index + vpp_session = VppBFDUDPSession(self, intf, intf.remote_ip4) + vpp_session.add_vpp_config() + vpp_session.admin_up() + intf.remove_vpp_config() + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.assert_equal(e.sw_if_index, sw_if_index, "sw_if_index") + self.assertFalse(vpp_session.query_vpp_config()) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFD6TestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) (IPv6) """ + + pg0 = None + vpp_clock_offset = None + vpp_session = None + test_session = None + + @classmethod + def setUpClass(cls): + super(BFD6TestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip6() + cls.pg0.configure_ipv6_neighbors() + cls.pg0.admin_up() + cls.pg0.resolve_ndp() + cls.create_loopback_interfaces([0]) + cls.loopback0 = cls.lo_interfaces[0] + cls.loopback0.config_ip6() + cls.loopback0.admin_up() + + except Exception: + super(BFD6TestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFD6TestCase, self).setUp() + self.factory = AuthKeyFactory() + self.vapi.want_bfd_events() + self.pg0.enable_capture() + try: + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip6, + af=AF_INET6) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0, AF_INET6) + self.logger.debug(self.vapi.cli("show adj nbr")) + except: + self.vapi.want_bfd_events(enable_disable=0) + raise + + def tearDown(self): + if not self.vpp_dead: + self.vapi.want_bfd_events(enable_disable=0) + self.vapi.collect_events() # clear the event queue + super(BFD6TestCase, self).tearDown() + + def test_session_up(self): + """ bring BFD session up """ + bfd_session_up(self) + + def test_session_up_by_ip(self): + """ bring BFD session up - first frame looked up by address pair """ + self.logger.info("BFD: Sending Slow control frame") + self.test_session.update(my_discriminator=randint(0, 40000000)) + self.test_session.send_packet() + self.pg0.enable_capture() + p = self.pg0.wait_for_packet(1) + self.assert_equal(p[BFD].your_discriminator, + self.test_session.my_discriminator, + "BFD - your discriminator") + self.assert_equal(p[BFD].state, BFDState.init, BFDState) + self.test_session.update(your_discriminator=p[BFD].my_discriminator, + state=BFDState.up) + self.logger.info("BFD: Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.init) + self.logger.info("BFD: Sending Up") + self.test_session.send_packet() + self.logger.info("BFD: Waiting for event") + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + verify_event(self, e, expected_state=BFDState.up) + self.logger.info("BFD: Session is Up") + self.test_session.update(state=BFDState.up) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_hold_up(self): + """ hold BFD session up """ + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + wait_for_bfd_packet(self) + self.test_session.send_packet() + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + def test_echo_looped_back(self): + """ echo packets looped back """ + # don't need a session in this case.. + self.vpp_session.remove_vpp_config() + self.pg0.enable_capture() + echo_packet_count = 10 + # random source port low enough to increment a few times.. + udp_sport_tx = randint(1, 50000) + udp_sport_rx = udp_sport_tx + echo_packet = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IPv6(src=self.pg0.remote_ip6, + dst=self.pg0.remote_ip6) / + UDP(dport=BFD.udp_dport_echo) / + Raw("this should be looped back")) + for dummy in range(echo_packet_count): + self.sleep(.01, "delay between echo packets") + echo_packet[UDP].sport = udp_sport_tx + udp_sport_tx += 1 + self.logger.debug(ppp("Sending packet:", echo_packet)) + self.pg0.add_stream(echo_packet) + self.pg_start() + for dummy in range(echo_packet_count): + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + ether = p[Ether] + self.assert_equal(self.pg0.remote_mac, + ether.dst, "Destination MAC") + self.assert_equal(self.pg0.local_mac, ether.src, "Source MAC") + ip = p[IPv6] + self.assert_equal(self.pg0.remote_ip6, ip.dst, "Destination IP") + self.assert_equal(self.pg0.remote_ip6, ip.src, "Destination IP") + udp = p[UDP] + self.assert_equal(udp.dport, BFD.udp_dport_echo, + "UDP destination port") + self.assert_equal(udp.sport, udp_sport_rx, "UDP source port") + udp_sport_rx += 1 + # need to compare the hex payload here, otherwise BFD_vpp_echo + # gets in way + self.assertEqual(str(p[UDP].payload), + str(echo_packet[UDP].payload), + "Received packet is not the echo packet sent") + self.assert_equal(udp_sport_tx, udp_sport_rx, "UDP source port (== " + "ECHO packet identifier for test purposes)") + self.assert_equal(udp_sport_tx, udp_sport_rx, "UDP source port (== " + "ECHO packet identifier for test purposes)") + + def test_echo(self): + """ echo function """ + bfd_session_up(self) + self.test_session.update(required_min_echo_rx=150000) + self.test_session.send_packet() + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + # echo shouldn't work without echo source set + for dummy in range(10): + sleep = self.vpp_session.required_min_rx / USEC_IN_SEC + self.sleep(sleep, "delay before sending bfd packet") + self.test_session.send_packet() + p = wait_for_bfd_packet( + self, pcap_time_min=time.time() - self.vpp_clock_offset) + self.assert_equal(p[BFD].required_min_rx_interval, + self.vpp_session.required_min_rx, + "BFD required min rx interval") + self.test_session.send_packet() + self.vapi.bfd_udp_set_echo_source(self.loopback0.sw_if_index) + echo_seen = False + # should be turned on - loopback echo packets + for dummy in range(3): + loop_until = time.time() + 0.75 * detection_time + while time.time() < loop_until: + p = self.pg0.wait_for_packet(1) + self.logger.debug(ppp("Got packet:", p)) + if p[UDP].dport == BFD.udp_dport_echo: + self.assert_equal( + p[IPv6].dst, self.pg0.local_ip6, "BFD ECHO dst IP") + self.assertNotEqual(p[IPv6].src, self.loopback0.local_ip6, + "BFD ECHO src IP equal to loopback IP") + self.logger.debug(ppp("Looping back packet:", p)) + self.assert_equal(p[Ether].dst, self.pg0.remote_mac, + "ECHO packet destination MAC address") + p[Ether].dst = self.pg0.local_mac + self.pg0.add_stream(p) + self.pg_start() + echo_seen = True + elif p.haslayer(BFD): + if echo_seen: + self.assertGreaterEqual( + p[BFD].required_min_rx_interval, + 1000000) + if "P" in p.sprintf("%BFD.flags%"): + final = self.test_session.create_packet() + final[BFD].flags = "F" + self.test_session.send_packet(final) + else: + raise Exception(ppp("Received unknown packet:", p)) + + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + self.test_session.send_packet() + self.assertTrue(echo_seen, "No echo packets received") + + def test_intf_deleted(self): + """ interface with bfd session deleted """ + intf = VppLoInterface(self, 0) + intf.config_ip6() + intf.admin_up() + sw_if_index = intf.sw_if_index + vpp_session = VppBFDUDPSession( + self, intf, intf.remote_ip6, af=AF_INET6) + vpp_session.add_vpp_config() + vpp_session.admin_up() + intf.remove_vpp_config() + e = self.vapi.wait_for_event(1, "bfd_udp_session_details") + self.assert_equal(e.sw_if_index, sw_if_index, "sw_if_index") + self.assertFalse(vpp_session.query_vpp_config()) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDFIBTestCase(VppTestCase): + """ BFD-FIB interactions (IPv6) """ + + vpp_session = None + test_session = None + + def setUp(self): + super(BFDFIBTestCase, self).setUp() + self.create_pg_interfaces(range(1)) + + self.vapi.want_bfd_events() + self.pg0.enable_capture() + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip6() + i.configure_ipv6_neighbors() + + def tearDown(self): + if not self.vpp_dead: + self.vapi.want_bfd_events(enable_disable=0) + + super(BFDFIBTestCase, self).tearDown() + + @staticmethod + def pkt_is_not_data_traffic(p): + """ not data traffic implies BFD or the usual IPv6 ND/RA""" + if p.haslayer(BFD) or is_ipv6_misc(p): + return True + return False + + def test_session_with_fib(self): + """ BFD-FIB interactions """ + + # packets to match against both of the routes + p = [(Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src="3001::1", dst="2001::1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)), + (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src="3001::1", dst="2002::1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100))] + + # A recursive and a non-recursive route via a next-hop that + # will have a BFD session + ip_2001_s_64 = VppIpRoute(self, "2001::", 64, + [VppRoutePath(self.pg0.remote_ip6, + self.pg0.sw_if_index, + proto=DPO_PROTO_IP6)], + is_ip6=1) + ip_2002_s_64 = VppIpRoute(self, "2002::", 64, + [VppRoutePath(self.pg0.remote_ip6, + 0xffffffff, + proto=DPO_PROTO_IP6)], + is_ip6=1) + ip_2001_s_64.add_vpp_config() + ip_2002_s_64.add_vpp_config() + + # bring the session up now the routes are present + self.vpp_session = VppBFDUDPSession(self, + self.pg0, + self.pg0.remote_ip6, + af=AF_INET6) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession(self, self.pg0, AF_INET6) + + # session is up - traffic passes + bfd_session_up(self) + + self.pg0.add_stream(p) + self.pg_start() + for packet in p: + captured = self.pg0.wait_for_packet( + 1, + filter_out_fn=self.pkt_is_not_data_traffic) + self.assertEqual(captured[IPv6].dst, + packet[IPv6].dst) + + # session is up - traffic is dropped + bfd_session_down(self) + + self.pg0.add_stream(p) + self.pg_start() + with self.assertRaises(CaptureTimeoutError): + self.pg0.wait_for_packet(1, self.pkt_is_not_data_traffic) + + # session is up - traffic passes + bfd_session_up(self) + + self.pg0.add_stream(p) + self.pg_start() + for packet in p: + captured = self.pg0.wait_for_packet( + 1, + filter_out_fn=self.pkt_is_not_data_traffic) + self.assertEqual(captured[IPv6].dst, + packet[IPv6].dst) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDSHA1TestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) (SHA1 auth) """ + + pg0 = None + vpp_clock_offset = None + vpp_session = None + test_session = None + + @classmethod + def setUpClass(cls): + super(BFDSHA1TestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip4() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFDSHA1TestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFDSHA1TestCase, self).setUp() + self.factory = AuthKeyFactory() + self.vapi.want_bfd_events() + self.pg0.enable_capture() + + def tearDown(self): + if not self.vpp_dead: + self.vapi.want_bfd_events(enable_disable=0) + self.vapi.collect_events() # clear the event queue + super(BFDSHA1TestCase, self).tearDown() + + def test_session_up(self): + """ bring BFD session up """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, + sha1_key=key) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_hold_up(self): + """ hold BFD session up """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, + sha1_key=key) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + wait_for_bfd_packet(self) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_hold_up_meticulous(self): + """ hold BFD session up - meticulous auth """ + key = self.factory.create_random_key( + self, BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + # specify sequence number so that it wraps + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id, + our_seq_number=0xFFFFFFFF - 4) + bfd_session_up(self) + for dummy in range(30): + wait_for_bfd_packet(self) + self.test_session.inc_seq_num() + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_send_bad_seq_number(self): + """ session is not kept alive by msgs with bad sequence numbers""" + key = self.factory.create_random_key( + self, BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + send_until = time.time() + 2 * detection_time + while time.time() < send_until: + self.test_session.send_packet() + self.sleep(0.7 * self.vpp_session.required_min_rx / USEC_IN_SEC, + "time between bfd packets") + e = self.vapi.collect_events() + # session should be down now, because the sequence numbers weren't + # updated + self.assert_equal(len(e), 1, "number of bfd events") + verify_event(self, e[0], expected_state=BFDState.down) + + def execute_rogue_session_scenario(self, vpp_bfd_udp_session, + legitimate_test_session, + rogue_test_session, + rogue_bfd_values=None): + """ execute a rogue session interaction scenario + + 1. create vpp session, add config + 2. bring the legitimate session up + 3. copy the bfd values from legitimate session to rogue session + 4. apply rogue_bfd_values to rogue session + 5. set rogue session state to down + 6. send message to take the session down from the rogue session + 7. assert that the legitimate session is unaffected + """ + + self.vpp_session = vpp_bfd_udp_session + self.vpp_session.add_vpp_config() + self.test_session = legitimate_test_session + # bring vpp session up + bfd_session_up(self) + # send packet from rogue session + rogue_test_session.update( + my_discriminator=self.test_session.my_discriminator, + your_discriminator=self.test_session.your_discriminator, + desired_min_tx=self.test_session.desired_min_tx, + required_min_rx=self.test_session.required_min_rx, + detect_mult=self.test_session.detect_mult, + diag=self.test_session.diag, + state=self.test_session.state, + auth_type=self.test_session.auth_type) + if rogue_bfd_values: + rogue_test_session.update(**rogue_bfd_values) + rogue_test_session.update(state=BFDState.down) + rogue_test_session.send_packet() + wait_for_bfd_packet(self) + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_mismatch_auth(self): + """ session is not brought down by unauthenticated msg """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=key) + legitimate_test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id) + rogue_test_session = BFDTestSession(self, self.pg0, AF_INET) + self.execute_rogue_session_scenario(vpp_session, + legitimate_test_session, + rogue_test_session) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_mismatch_bfd_key_id(self): + """ session is not brought down by msg with non-existent key-id """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=key) + # pick a different random bfd key id + x = randint(0, 255) + while x == vpp_session.bfd_key_id: + x = randint(0, 255) + legitimate_test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id) + rogue_test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, bfd_key_id=x) + self.execute_rogue_session_scenario(vpp_session, + legitimate_test_session, + rogue_test_session) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_mismatched_auth_type(self): + """ session is not brought down by msg with wrong auth type """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=key) + legitimate_test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id) + rogue_test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id) + self.execute_rogue_session_scenario( + vpp_session, legitimate_test_session, rogue_test_session, + {'auth_type': BFDAuthType.keyed_md5}) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_restart(self): + """ simulate remote peer restart and resynchronization """ + key = self.factory.create_random_key( + self, BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id, our_seq_number=0) + bfd_session_up(self) + # don't send any packets for 2*detection_time + detection_time = self.test_session.detect_mult *\ + self.vpp_session.required_min_rx / USEC_IN_SEC + self.sleep(2 * detection_time, "simulating peer restart") + events = self.vapi.collect_events() + self.assert_equal(len(events), 1, "number of bfd events") + verify_event(self, events[0], expected_state=BFDState.down) + self.test_session.update(state=BFDState.down) + # reset sequence number + self.test_session.our_seq_number = 0 + self.test_session.vpp_seq_number = None + # now throw away any pending packets + self.pg0.enable_capture() + bfd_session_up(self) + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDAuthOnOffTestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) (changing auth) """ + + pg0 = None + vpp_session = None + test_session = None + + @classmethod + def setUpClass(cls): + super(BFDAuthOnOffTestCase, cls).setUpClass() + try: + cls.create_pg_interfaces([0]) + cls.pg0.config_ip4() + cls.pg0.admin_up() + cls.pg0.resolve_arp() + + except Exception: + super(BFDAuthOnOffTestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFDAuthOnOffTestCase, self).setUp() + self.factory = AuthKeyFactory() + self.vapi.want_bfd_events() + self.pg0.enable_capture() + + def tearDown(self): + if not self.vpp_dead: + self.vapi.want_bfd_events(enable_disable=0) + self.vapi.collect_events() # clear the event queue + super(BFDAuthOnOffTestCase, self).tearDown() + + def test_auth_on_immediate(self): + """ turn auth on without disturbing session state (immediate) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession(self, self.pg0, AF_INET) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.vpp_session.activate_auth(key) + self.test_session.bfd_key_id = self.vpp_session.bfd_key_id + self.test_session.sha1_key = key + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + def test_auth_off_immediate(self): + """ turn auth off without disturbing session state (immediate) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + # self.vapi.want_bfd_events(enable_disable=0) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.inc_seq_num() + self.test_session.send_packet() + self.vpp_session.deactivate_auth() + self.test_session.bfd_key_id = None + self.test_session.sha1_key = None + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.inc_seq_num() + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + def test_auth_change_key_immediate(self): + """ change auth key without disturbing session state (immediate) """ + key1 = self.factory.create_random_key(self) + key1.add_vpp_config() + key2 = self.factory.create_random_key(self) + key2.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key1) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key1, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.vpp_session.activate_auth(key2) + self.test_session.bfd_key_id = self.vpp_session.bfd_key_id + self.test_session.sha1_key = key2 + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + def test_auth_on_delayed(self): + """ turn auth on without disturbing session state (delayed) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession(self, self.pg0, AF_INET) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + wait_for_bfd_packet(self) + self.test_session.send_packet() + self.vpp_session.activate_auth(key, delayed=True) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.test_session.bfd_key_id = self.vpp_session.bfd_key_id + self.test_session.sha1_key = key + self.test_session.send_packet() + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + def test_auth_off_delayed(self): + """ turn auth off without disturbing session state (delayed) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key) + self.vpp_session.add_vpp_config() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.vpp_session.deactivate_auth(delayed=True) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.test_session.bfd_key_id = None + self.test_session.sha1_key = None + self.test_session.send_packet() + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + def test_auth_change_key_delayed(self): + """ change auth key without disturbing session state (delayed) """ + key1 = self.factory.create_random_key(self) + key1.add_vpp_config() + key2 = self.factory.create_random_key(self) + key2.add_vpp_config() + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip4, sha1_key=key1) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = BFDTestSession( + self, self.pg0, AF_INET, sha1_key=key1, + bfd_key_id=self.vpp_session.bfd_key_id) + bfd_session_up(self) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.vpp_session.activate_auth(key2, delayed=True) + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.test_session.bfd_key_id = self.vpp_session.bfd_key_id + self.test_session.sha1_key = key2 + self.test_session.send_packet() + for dummy in range(self.test_session.detect_mult * 2): + p = wait_for_bfd_packet(self) + self.assert_equal(p[BFD].state, BFDState.up, BFDState) + self.test_session.send_packet() + self.assert_equal(self.vpp_session.state, BFDState.up, BFDState) + self.assert_equal(len(self.vapi.collect_events()), 0, + "number of bfd events") + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class BFDCLITestCase(VppTestCase): + """Bidirectional Forwarding Detection (BFD) (CLI) """ + pg0 = None + + @classmethod + def setUpClass(cls): + super(BFDCLITestCase, cls).setUpClass() + + try: + cls.create_pg_interfaces((0,)) + cls.pg0.config_ip4() + cls.pg0.config_ip6() + cls.pg0.resolve_arp() + cls.pg0.resolve_ndp() + + except Exception: + super(BFDCLITestCase, cls).tearDownClass() + raise + + def setUp(self): + super(BFDCLITestCase, self).setUp() + self.factory = AuthKeyFactory() + self.pg0.enable_capture() + + def tearDown(self): + try: + self.vapi.want_bfd_events(enable_disable=0) + except UnexpectedApiReturnValueError: + # some tests aren't subscribed, so this is not an issue + pass + self.vapi.collect_events() # clear the event queue + super(BFDCLITestCase, self).tearDown() + + def cli_verify_no_response(self, cli): + """ execute a CLI, asserting that the response is empty """ + self.assert_equal(self.vapi.cli(cli), + "", + "CLI command response") + + def cli_verify_response(self, cli, expected): + """ execute a CLI, asserting that the response matches expectation """ + self.assert_equal(self.vapi.cli(cli).strip(), + expected, + "CLI command response") + + def test_show(self): + """ show commands """ + k1 = self.factory.create_random_key(self) + k1.add_vpp_config() + k2 = self.factory.create_random_key( + self, auth_type=BFDAuthType.meticulous_keyed_sha1) + k2.add_vpp_config() + s1 = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + s1.add_vpp_config() + s2 = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip6, af=AF_INET6, + sha1_key=k2) + s2.add_vpp_config() + self.logger.info(self.vapi.ppcli("show bfd keys")) + self.logger.info(self.vapi.ppcli("show bfd sessions")) + self.logger.info(self.vapi.ppcli("show bfd")) + + def test_set_del_sha1_key(self): + """ set/delete SHA1 auth key """ + k = self.factory.create_random_key(self) + self.registry.register(k, self.logger) + self.cli_verify_no_response( + "bfd key set conf-key-id %s type keyed-sha1 secret %s" % + (k.conf_key_id, + "".join("{:02x}".format(ord(c)) for c in k.key))) + self.assertTrue(k.query_vpp_config()) + self.vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=k) + self.vpp_session.add_vpp_config() + self.test_session = \ + BFDTestSession(self, self.pg0, AF_INET, sha1_key=k, + bfd_key_id=self.vpp_session.bfd_key_id) + self.vapi.want_bfd_events() + bfd_session_up(self) + bfd_session_down(self) + # try to replace the secret for the key - should fail because the key + # is in-use + k2 = self.factory.create_random_key(self) + self.cli_verify_response( + "bfd key set conf-key-id %s type keyed-sha1 secret %s" % + (k.conf_key_id, + "".join("{:02x}".format(ord(c)) for c in k2.key)), + "bfd key set: `bfd_auth_set_key' API call failed, " + "rv=-103:BFD object in use") + # manipulating the session using old secret should still work + bfd_session_up(self) + bfd_session_down(self) + self.vpp_session.remove_vpp_config() + self.cli_verify_no_response( + "bfd key del conf-key-id %s" % k.conf_key_id) + self.assertFalse(k.query_vpp_config()) + + def test_set_del_meticulous_sha1_key(self): + """ set/delete meticulous SHA1 auth key """ + k = self.factory.create_random_key( + self, auth_type=BFDAuthType.meticulous_keyed_sha1) + self.registry.register(k, self.logger) + self.cli_verify_no_response( + "bfd key set conf-key-id %s type meticulous-keyed-sha1 secret %s" % + (k.conf_key_id, + "".join("{:02x}".format(ord(c)) for c in k.key))) + self.assertTrue(k.query_vpp_config()) + self.vpp_session = VppBFDUDPSession(self, self.pg0, + self.pg0.remote_ip6, af=AF_INET6, + sha1_key=k) + self.vpp_session.add_vpp_config() + self.vpp_session.admin_up() + self.test_session = \ + BFDTestSession(self, self.pg0, AF_INET6, sha1_key=k, + bfd_key_id=self.vpp_session.bfd_key_id) + self.vapi.want_bfd_events() + bfd_session_up(self) + bfd_session_down(self) + # try to replace the secret for the key - should fail because the key + # is in-use + k2 = self.factory.create_random_key(self) + self.cli_verify_response( + "bfd key set conf-key-id %s type keyed-sha1 secret %s" % + (k.conf_key_id, + "".join("{:02x}".format(ord(c)) for c in k2.key)), + "bfd key set: `bfd_auth_set_key' API call failed, " + "rv=-103:BFD object in use") + # manipulating the session using old secret should still work + bfd_session_up(self) + bfd_session_down(self) + self.vpp_session.remove_vpp_config() + self.cli_verify_no_response( + "bfd key del conf-key-id %s" % k.conf_key_id) + self.assertFalse(k.query_vpp_config()) + + def test_add_mod_del_bfd_udp(self): + """ create/modify/delete IPv4 BFD UDP session """ + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4) + self.registry.register(vpp_session, self.logger) + cli_add_cmd = "bfd udp session add interface %s local-addr %s " \ + "peer-addr %s desired-min-tx %s required-min-rx %s "\ + "detect-mult %s" % (self.pg0.name, self.pg0.local_ip4, + self.pg0.remote_ip4, + vpp_session.desired_min_tx, + vpp_session.required_min_rx, + vpp_session.detect_mult) + self.cli_verify_no_response(cli_add_cmd) + # 2nd add should fail + self.cli_verify_response( + cli_add_cmd, + "bfd udp session add: `bfd_add_add_session' API call" + " failed, rv=-101:Duplicate BFD object") + verify_bfd_session_config(self, vpp_session) + mod_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, + required_min_rx=2 * vpp_session.required_min_rx, + desired_min_tx=3 * vpp_session.desired_min_tx, + detect_mult=4 * vpp_session.detect_mult) + self.cli_verify_no_response( + "bfd udp session mod interface %s local-addr %s peer-addr %s " + "desired-min-tx %s required-min-rx %s detect-mult %s" % + (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4, + mod_session.desired_min_tx, mod_session.required_min_rx, + mod_session.detect_mult)) + verify_bfd_session_config(self, mod_session) + cli_del_cmd = "bfd udp session del interface %s local-addr %s "\ + "peer-addr %s" % (self.pg0.name, + self.pg0.local_ip4, self.pg0.remote_ip4) + self.cli_verify_no_response(cli_del_cmd) + # 2nd del is expected to fail + self.cli_verify_response( + cli_del_cmd, "bfd udp session del: `bfd_udp_del_session' API call" + " failed, rv=-102:No such BFD object") + self.assertFalse(vpp_session.query_vpp_config()) + + def test_add_mod_del_bfd_udp6(self): + """ create/modify/delete IPv6 BFD UDP session """ + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip6, af=AF_INET6) + self.registry.register(vpp_session, self.logger) + cli_add_cmd = "bfd udp session add interface %s local-addr %s " \ + "peer-addr %s desired-min-tx %s required-min-rx %s "\ + "detect-mult %s" % (self.pg0.name, self.pg0.local_ip6, + self.pg0.remote_ip6, + vpp_session.desired_min_tx, + vpp_session.required_min_rx, + vpp_session.detect_mult) + self.cli_verify_no_response(cli_add_cmd) + # 2nd add should fail + self.cli_verify_response( + cli_add_cmd, + "bfd udp session add: `bfd_add_add_session' API call" + " failed, rv=-101:Duplicate BFD object") + verify_bfd_session_config(self, vpp_session) + mod_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip6, af=AF_INET6, + required_min_rx=2 * vpp_session.required_min_rx, + desired_min_tx=3 * vpp_session.desired_min_tx, + detect_mult=4 * vpp_session.detect_mult) + self.cli_verify_no_response( + "bfd udp session mod interface %s local-addr %s peer-addr %s " + "desired-min-tx %s required-min-rx %s detect-mult %s" % + (self.pg0.name, self.pg0.local_ip6, self.pg0.remote_ip6, + mod_session.desired_min_tx, + mod_session.required_min_rx, mod_session.detect_mult)) + verify_bfd_session_config(self, mod_session) + cli_del_cmd = "bfd udp session del interface %s local-addr %s "\ + "peer-addr %s" % (self.pg0.name, + self.pg0.local_ip6, self.pg0.remote_ip6) + self.cli_verify_no_response(cli_del_cmd) + # 2nd del is expected to fail + self.cli_verify_response( + cli_del_cmd, + "bfd udp session del: `bfd_udp_del_session' API call" + " failed, rv=-102:No such BFD object") + self.assertFalse(vpp_session.query_vpp_config()) + + def test_add_mod_del_bfd_udp_auth(self): + """ create/modify/delete IPv4 BFD UDP session (authenticated) """ + key = self.factory.create_random_key(self) + key.add_vpp_config() + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=key) + self.registry.register(vpp_session, self.logger) + cli_add_cmd = "bfd udp session add interface %s local-addr %s " \ + "peer-addr %s desired-min-tx %s required-min-rx %s "\ + "detect-mult %s conf-key-id %s bfd-key-id %s"\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4, + vpp_session.desired_min_tx, vpp_session.required_min_rx, + vpp_session.detect_mult, key.conf_key_id, + vpp_session.bfd_key_id) + self.cli_verify_no_response(cli_add_cmd) + # 2nd add should fail + self.cli_verify_response( + cli_add_cmd, + "bfd udp session add: `bfd_add_add_session' API call" + " failed, rv=-101:Duplicate BFD object") + verify_bfd_session_config(self, vpp_session) + mod_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip4, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id, + required_min_rx=2 * vpp_session.required_min_rx, + desired_min_tx=3 * vpp_session.desired_min_tx, + detect_mult=4 * vpp_session.detect_mult) + self.cli_verify_no_response( + "bfd udp session mod interface %s local-addr %s peer-addr %s " + "desired-min-tx %s required-min-rx %s detect-mult %s" % + (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4, + mod_session.desired_min_tx, + mod_session.required_min_rx, mod_session.detect_mult)) + verify_bfd_session_config(self, mod_session) + cli_del_cmd = "bfd udp session del interface %s local-addr %s "\ + "peer-addr %s" % (self.pg0.name, + self.pg0.local_ip4, self.pg0.remote_ip4) + self.cli_verify_no_response(cli_del_cmd) + # 2nd del is expected to fail + self.cli_verify_response( + cli_del_cmd, + "bfd udp session del: `bfd_udp_del_session' API call" + " failed, rv=-102:No such BFD object") + self.assertFalse(vpp_session.query_vpp_config()) + + def test_add_mod_del_bfd_udp6_auth(self): + """ create/modify/delete IPv6 BFD UDP session (authenticated) """ + key = self.factory.create_random_key( + self, auth_type=BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + vpp_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip6, af=AF_INET6, sha1_key=key) + self.registry.register(vpp_session, self.logger) + cli_add_cmd = "bfd udp session add interface %s local-addr %s " \ + "peer-addr %s desired-min-tx %s required-min-rx %s "\ + "detect-mult %s conf-key-id %s bfd-key-id %s" \ + % (self.pg0.name, self.pg0.local_ip6, self.pg0.remote_ip6, + vpp_session.desired_min_tx, vpp_session.required_min_rx, + vpp_session.detect_mult, key.conf_key_id, + vpp_session.bfd_key_id) + self.cli_verify_no_response(cli_add_cmd) + # 2nd add should fail + self.cli_verify_response( + cli_add_cmd, + "bfd udp session add: `bfd_add_add_session' API call" + " failed, rv=-101:Duplicate BFD object") + verify_bfd_session_config(self, vpp_session) + mod_session = VppBFDUDPSession( + self, self.pg0, self.pg0.remote_ip6, af=AF_INET6, sha1_key=key, + bfd_key_id=vpp_session.bfd_key_id, + required_min_rx=2 * vpp_session.required_min_rx, + desired_min_tx=3 * vpp_session.desired_min_tx, + detect_mult=4 * vpp_session.detect_mult) + self.cli_verify_no_response( + "bfd udp session mod interface %s local-addr %s peer-addr %s " + "desired-min-tx %s required-min-rx %s detect-mult %s" % + (self.pg0.name, self.pg0.local_ip6, self.pg0.remote_ip6, + mod_session.desired_min_tx, + mod_session.required_min_rx, mod_session.detect_mult)) + verify_bfd_session_config(self, mod_session) + cli_del_cmd = "bfd udp session del interface %s local-addr %s "\ + "peer-addr %s" % (self.pg0.name, + self.pg0.local_ip6, self.pg0.remote_ip6) + self.cli_verify_no_response(cli_del_cmd) + # 2nd del is expected to fail + self.cli_verify_response( + cli_del_cmd, + "bfd udp session del: `bfd_udp_del_session' API call" + " failed, rv=-102:No such BFD object") + self.assertFalse(vpp_session.query_vpp_config()) + + def test_auth_on_off(self): + """ turn authentication on and off """ + key = self.factory.create_random_key( + self, auth_type=BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + auth_session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key) + session.add_vpp_config() + cli_activate = \ + "bfd udp session auth activate interface %s local-addr %s "\ + "peer-addr %s conf-key-id %s bfd-key-id %s"\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4, + key.conf_key_id, auth_session.bfd_key_id) + self.cli_verify_no_response(cli_activate) + verify_bfd_session_config(self, auth_session) + self.cli_verify_no_response(cli_activate) + verify_bfd_session_config(self, auth_session) + cli_deactivate = \ + "bfd udp session auth deactivate interface %s local-addr %s "\ + "peer-addr %s "\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4) + self.cli_verify_no_response(cli_deactivate) + verify_bfd_session_config(self, session) + self.cli_verify_no_response(cli_deactivate) + verify_bfd_session_config(self, session) + + def test_auth_on_off_delayed(self): + """ turn authentication on and off (delayed) """ + key = self.factory.create_random_key( + self, auth_type=BFDAuthType.meticulous_keyed_sha1) + key.add_vpp_config() + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + auth_session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4, + sha1_key=key) + session.add_vpp_config() + cli_activate = \ + "bfd udp session auth activate interface %s local-addr %s "\ + "peer-addr %s conf-key-id %s bfd-key-id %s delayed yes"\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4, + key.conf_key_id, auth_session.bfd_key_id) + self.cli_verify_no_response(cli_activate) + verify_bfd_session_config(self, auth_session) + self.cli_verify_no_response(cli_activate) + verify_bfd_session_config(self, auth_session) + cli_deactivate = \ + "bfd udp session auth deactivate interface %s local-addr %s "\ + "peer-addr %s delayed yes"\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4) + self.cli_verify_no_response(cli_deactivate) + verify_bfd_session_config(self, session) + self.cli_verify_no_response(cli_deactivate) + verify_bfd_session_config(self, session) + + def test_admin_up_down(self): + """ put session admin-up and admin-down """ + session = VppBFDUDPSession(self, self.pg0, self.pg0.remote_ip4) + session.add_vpp_config() + cli_down = \ + "bfd udp session set-flags admin down interface %s local-addr %s "\ + "peer-addr %s "\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4) + cli_up = \ + "bfd udp session set-flags admin up interface %s local-addr %s "\ + "peer-addr %s "\ + % (self.pg0.name, self.pg0.local_ip4, self.pg0.remote_ip4) + self.cli_verify_no_response(cli_down) + verify_bfd_session_config(self, session, state=BFDState.admin_down) + self.cli_verify_no_response(cli_up) + verify_bfd_session_config(self, session, state=BFDState.down) + + def test_set_del_udp_echo_source(self): + """ set/del udp echo source """ + self.create_loopback_interfaces([0]) + self.loopback0 = self.lo_interfaces[0] + self.loopback0.admin_up() + self.cli_verify_response("show bfd echo-source", + "UDP echo source is not set.") + cli_set = "bfd udp echo-source set interface %s" % self.loopback0.name + self.cli_verify_no_response(cli_set) + self.cli_verify_response("show bfd echo-source", + "UDP echo source is: %s\n" + "IPv4 address usable as echo source: none\n" + "IPv6 address usable as echo source: none" % + self.loopback0.name) + self.loopback0.config_ip4() + unpacked = unpack("!L", self.loopback0.local_ip4n) + echo_ip4 = inet_ntop(AF_INET, pack("!L", unpacked[0] ^ 1)) + self.cli_verify_response("show bfd echo-source", + "UDP echo source is: %s\n" + "IPv4 address usable as echo source: %s\n" + "IPv6 address usable as echo source: none" % + (self.loopback0.name, echo_ip4)) + unpacked = unpack("!LLLL", self.loopback0.local_ip6n) + echo_ip6 = inet_ntop(AF_INET6, pack("!LLLL", unpacked[0], unpacked[1], + unpacked[2], unpacked[3] ^ 1)) + self.loopback0.config_ip6() + self.cli_verify_response("show bfd echo-source", + "UDP echo source is: %s\n" + "IPv4 address usable as echo source: %s\n" + "IPv6 address usable as echo source: %s" % + (self.loopback0.name, echo_ip4, echo_ip6)) + cli_del = "bfd udp echo-source del" + self.cli_verify_no_response(cli_del) + self.cli_verify_response("show bfd echo-source", + "UDP echo source is not set.") + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_classifier.py b/test/test_classifier.py new file mode 100644 index 00000000..a43f7a3d --- /dev/null +++ b/test/test_classifier.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python + +import unittest +import socket +import binascii + +from framework import VppTestCase, VppTestRunner + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP +from util import ppp + + +class TestClassifier(VppTestCase): + """ Classifier Test Case """ + + def setUp(self): + """ + Perform test setup before test case. + + **Config:** + - create 4 pg interfaces + - untagged pg0/pg1/pg2 interface + pg0 -------> pg1 (IP ACL) + \ + ---> pg2 (MAC ACL)) + \ + -> pg3 (PBR) + - setup interfaces: + - put it into UP state + - set IPv4 addresses + - resolve neighbor address using ARP + + :ivar list interfaces: pg interfaces. + :ivar list pg_if_packet_sizes: packet sizes in test. + :ivar dict acl_tbl_idx: ACL table index. + :ivar int pbr_vrfid: VRF id for PBR test. + """ + super(TestClassifier, self).setUp() + + # create 4 pg interfaces + self.create_pg_interfaces(range(4)) + + # packet sizes to test + self.pg_if_packet_sizes = [64, 9018] + + self.interfaces = list(self.pg_interfaces) + + # ACL & PBR vars + self.acl_tbl_idx = {} + self.pbr_vrfid = 200 + + # setup all interfaces + for intf in self.interfaces: + intf.admin_up() + intf.config_ip4() + intf.resolve_arp() + + def tearDown(self): + """Run standard test teardown and acl related log.""" + super(TestClassifier, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show classify table verbose")) + self.logger.info(self.vapi.cli("show ip fib")) + + def config_pbr_fib_entry(self, intf, is_add=1): + """Configure fib entry to route traffic toward PBR VRF table + + :param VppInterface intf: destination interface to be routed for PBR. + + """ + addr_len = 24 + self.vapi.ip_add_del_route(intf.local_ip4n, + addr_len, + intf.remote_ip4n, + table_id=self.pbr_vrfid, + is_add=is_add) + + def create_stream(self, src_if, dst_if, packet_sizes): + """Create input packet stream for defined interfaces. + + :param VppInterface src_if: Source Interface for packet stream. + :param VppInterface dst_if: Destination Interface for packet stream. + :param list packet_sizes: packet size to test. + """ + pkts = [] + for size in packet_sizes: + info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / + UDP(sport=1234, dport=5678) / + Raw(payload)) + info.data = p.copy() + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, dst_if, capture): + """Verify captured input packet stream for defined interface. + + :param VppInterface dst_if: Interface to verify captured packet stream. + :param list capture: Captured packet stream. + """ + self.logger.info("Verifying capture on interface %s" % dst_if.name) + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = dst_if.sw_if_index + for packet in capture: + try: + ip = packet[IP] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug( + "Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) + + def verify_vrf(self, vrf_id): + """ + Check if the FIB table / VRF ID is configured. + + :param int vrf_id: The FIB table / VRF ID to be verified. + :return: 1 if the FIB table / VRF ID is configured, otherwise return 0. + """ + ip_fib_dump = self.vapi.ip_fib_dump() + vrf_count = 0 + for ip_fib_details in ip_fib_dump: + if ip_fib_details[2] == vrf_id: + vrf_count += 1 + if vrf_count == 0: + self.logger.info("IPv4 VRF ID %d is not configured" % vrf_id) + return 0 + else: + self.logger.info("IPv4 VRF ID %d is configured" % vrf_id) + return 1 + + @staticmethod + def build_ip_mask(proto='', src_ip='', dst_ip='', + src_port='', dst_port=''): + """Build IP ACL mask data with hexstring format + + :param str proto: protocol number <0-ff> + :param str src_ip: source ip address <0-ffffffff> + :param str dst_ip: destination ip address <0-ffffffff> + :param str src_port: source port number <0-ffff> + :param str dst_port: destination port number <0-ffff> + """ + + return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format( + proto, src_ip, dst_ip, src_port, dst_port)).rstrip('0') + + @staticmethod + def build_ip_match(proto='', src_ip='', dst_ip='', + src_port='', dst_port=''): + """Build IP ACL match data with hexstring format + + :param str proto: protocol number with valid option "<0-ff>" + :param str src_ip: source ip address with format of "x.x.x.x" + :param str dst_ip: destination ip address with format of "x.x.x.x" + :param str src_port: source port number <0-ffff> + :param str dst_port: destination port number <0-ffff> + """ + if src_ip: + src_ip = socket.inet_aton(src_ip).encode('hex') + if dst_ip: + dst_ip = socket.inet_aton(dst_ip).encode('hex') + + return ('{:0>20}{:0>12}{:0>8}{:0>12}{:0>4}'.format( + proto, src_ip, dst_ip, src_port, dst_port)).rstrip('0') + + @staticmethod + def build_mac_mask(dst_mac='', src_mac='', ether_type=''): + """Build MAC ACL mask data with hexstring format + + :param str dst_mac: source MAC address <0-ffffffffffff> + :param str src_mac: destination MAC address <0-ffffffffffff> + :param str ether_type: ethernet type <0-ffff> + """ + + return ('{:0>12}{:0>12}{:0>4}'.format(dst_mac, src_mac, + ether_type)).rstrip('0') + + @staticmethod + def build_mac_match(dst_mac='', src_mac='', ether_type=''): + """Build MAC ACL match data with hexstring format + + :param str dst_mac: source MAC address <x:x:x:x:x:x> + :param str src_mac: destination MAC address <x:x:x:x:x:x> + :param str ether_type: ethernet type <0-ffff> + """ + if dst_mac: + dst_mac = dst_mac.replace(':', '') + if src_mac: + src_mac = src_mac.replace(':', '') + + return ('{:0>12}{:0>12}{:0>4}'.format(dst_mac, src_mac, + ether_type)).rstrip('0') + + def create_classify_table(self, key, mask, data_offset=0, is_add=1): + """Create Classify Table + + :param str key: key for classify table (ex, ACL name). + :param str mask: mask value for interested traffic. + :param int match_n_vectors: + :param int is_add: option to configure classify table. + - create(1) or delete(0) + """ + r = self.vapi.classify_add_del_table( + is_add, + binascii.unhexlify(mask), + match_n_vectors=(len(mask) - 1) // 32 + 1, + miss_next_index=0, + current_data_flag=1, + current_data_offset=data_offset) + self.assertIsNotNone(r, msg='No response msg for add_del_table') + self.acl_tbl_idx[key] = r.new_table_index + + def create_classify_session(self, intf, table_index, match, + pbr_option=0, vrfid=0, is_add=1): + """Create Classify Session + + :param VppInterface intf: Interface to apply classify session. + :param int table_index: table index to identify classify table. + :param str match: matched value for interested traffic. + :param int pbr_action: enable/disable PBR feature. + :param int vrfid: VRF id. + :param int is_add: option to configure classify session. + - create(1) or delete(0) + """ + r = self.vapi.classify_add_del_session( + is_add, + table_index, + binascii.unhexlify(match), + opaque_index=0, + action=pbr_option, + metadata=vrfid) + self.assertIsNotNone(r, msg='No response msg for add_del_session') + + def input_acl_set_interface(self, intf, table_index, is_add=1): + """Configure Input ACL interface + + :param VppInterface intf: Interface to apply Input ACL feature. + :param int table_index: table index to identify classify table. + :param int is_add: option to configure classify session. + - enable(1) or disable(0) + """ + r = self.vapi.input_acl_set_interface( + is_add, + intf.sw_if_index, + ip4_table_index=table_index) + self.assertIsNotNone(r, msg='No response msg for acl_set_interface') + + def test_acl_ip(self): + """ IP ACL test + + Test scenario for basic IP ACL with source IP + - Create IPv4 stream for pg0 -> pg1 interface. + - Create ACL with source IP address. + - Send and verify received packets on pg1 interface. + """ + + # Basic ACL testing with source IP + pkts = self.create_stream(self.pg0, self.pg1, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + self.create_classify_table('ip', self.build_ip_mask(src_ip='ffffffff')) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('ip'), + self.build_ip_match(src_ip=self.pg0.remote_ip4)) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('ip')) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg1.get_capture(len(pkts)) + self.verify_capture(self.pg1, pkts) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('ip'), 0) + self.pg0.assert_nothing_captured(remark="packets forwarded") + self.pg2.assert_nothing_captured(remark="packets forwarded") + self.pg3.assert_nothing_captured(remark="packets forwarded") + + def test_acl_mac(self): + """ MAC ACL test + + Test scenario for basic MAC ACL with source MAC + - Create IPv4 stream for pg0 -> pg2 interface. + - Create ACL with source MAC address. + - Send and verify received packets on pg2 interface. + """ + + # Basic ACL testing with source MAC + pkts = self.create_stream(self.pg0, self.pg2, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + self.create_classify_table('mac', + self.build_mac_mask(src_mac='ffffffffffff'), + data_offset=-14) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('mac'), + self.build_mac_match(src_mac=self.pg0.remote_mac)) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('mac')) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg2.get_capture(len(pkts)) + self.verify_capture(self.pg2, pkts) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('mac'), 0) + self.pg0.assert_nothing_captured(remark="packets forwarded") + self.pg1.assert_nothing_captured(remark="packets forwarded") + self.pg3.assert_nothing_captured(remark="packets forwarded") + + def test_acl_pbr(self): + """ IP PBR test + + Test scenario for PBR with source IP + - Create IPv4 stream for pg0 -> pg3 interface. + - Configure PBR fib entry for packet forwarding. + - Send and verify received packets on pg3 interface. + """ + + # PBR testing with source IP + pkts = self.create_stream(self.pg0, self.pg3, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + self.create_classify_table( + 'pbr', self.build_ip_mask( + src_ip='ffffffff')) + pbr_option = 1 + # this will create the VRF/table in which we will insert the route + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('pbr'), + self.build_ip_match(src_ip=self.pg0.remote_ip4), + pbr_option, self.pbr_vrfid) + self.assertTrue(self.verify_vrf(self.pbr_vrfid)) + self.config_pbr_fib_entry(self.pg3) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('pbr')) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg3.get_capture(len(pkts)) + self.verify_capture(self.pg3, pkts) + self.input_acl_set_interface(self.pg0, self.acl_tbl_idx.get('pbr'), 0) + self.pg0.assert_nothing_captured(remark="packets forwarded") + self.pg1.assert_nothing_captured(remark="packets forwarded") + self.pg2.assert_nothing_captured(remark="packets forwarded") + + # remove the classify session and the route + self.config_pbr_fib_entry(self.pg3, is_add=0) + self.create_classify_session( + self.pg0, self.acl_tbl_idx.get('pbr'), + self.build_ip_match(src_ip=self.pg0.remote_ip4), + pbr_option, self.pbr_vrfid, is_add=0) + + # and the table should be gone. + self.assertFalse(self.verify_vrf(self.pbr_vrfid)) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_dhcp.py b/test/test_dhcp.py new file mode 100644 index 00000000..42b80af3 --- /dev/null +++ b/test/test_dhcp.py @@ -0,0 +1,1209 @@ +#!/usr/bin/env python + +import unittest +import socket +import struct + +from framework import VppTestCase, VppTestRunner +from vpp_neighbor import VppNeighbor +from vpp_ip_route import find_route, VppIpTable +from util import mk_ll_addr + +from scapy.layers.l2 import Ether, getmacbyip, ARP +from scapy.layers.inet import IP, UDP, ICMP +from scapy.layers.inet6 import IPv6, in6_getnsmac, in6_mactoifaceid +from scapy.layers.dhcp import DHCP, BOOTP, DHCPTypes +from scapy.layers.dhcp6 import DHCP6, DHCP6_Solicit, DHCP6_RelayForward, \ + DHCP6_RelayReply, DHCP6_Advertise, DHCP6OptRelayMsg, DHCP6OptIfaceId, \ + DHCP6OptStatusCode, DHCP6OptVSS, DHCP6OptClientLinkLayerAddr, DHCP6_Request +from socket import AF_INET, AF_INET6 +from scapy.utils import inet_pton, inet_ntop +from scapy.utils6 import in6_ptop +from util import mactobinary + +DHCP4_CLIENT_PORT = 68 +DHCP4_SERVER_PORT = 67 +DHCP6_CLIENT_PORT = 547 +DHCP6_SERVER_PORT = 546 + + +class TestDHCP(VppTestCase): + """ DHCP Test Case """ + + def setUp(self): + super(TestDHCP, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(4)) + self.tables = [] + + # pg0 and 1 are IP configured in VRF 0 and 1. + # pg2 and 3 are non IP-configured in VRF 0 and 1 + table_id = 0 + for table_id in range(1, 4): + tbl4 = VppIpTable(self, table_id) + tbl4.add_vpp_config() + self.tables.append(tbl4) + tbl6 = VppIpTable(self, table_id, is_ip6=1) + tbl6.add_vpp_config() + self.tables.append(tbl6) + + table_id = 0 + for i in self.pg_interfaces[:2]: + i.admin_up() + i.set_table_ip4(table_id) + i.set_table_ip6(table_id) + i.config_ip4() + i.resolve_arp() + i.config_ip6() + i.resolve_ndp() + table_id += 1 + + table_id = 0 + for i in self.pg_interfaces[2:]: + i.admin_up() + i.set_table_ip4(table_id) + i.set_table_ip6(table_id) + table_id += 1 + + def tearDown(self): + for i in self.pg_interfaces[:2]: + i.unconfig_ip4() + i.unconfig_ip6() + + for i in self.pg_interfaces: + i.set_table_ip4(0) + i.set_table_ip6(0) + i.admin_down() + super(TestDHCP, self).tearDown() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.assert_nothing_captured(remark=remark) + + def verify_dhcp_has_option(self, pkt, option, value): + dhcp = pkt[DHCP] + found = False + + for i in dhcp.options: + if type(i) is tuple: + if i[0] == option: + self.assertEqual(i[1], value) + found = True + + self.assertTrue(found) + + def validate_relay_options(self, pkt, intf, ip_addr, fib_id, oui): + dhcp = pkt[DHCP] + found = 0 + data = [] + + for i in dhcp.options: + if type(i) is tuple: + if i[0] == "relay_agent_Information": + # + # There are two sb-options present - each of length 6. + # + data = i[1] + if oui != 0: + self.assertEqual(len(data), 24) + else: + self.assertEqual(len(data), 12) + + # + # First sub-option is ID 1, len 4, then encoded + # sw_if_index. This test uses low valued indicies + # so [2:4] are 0. + # The ID space is VPP internal - so no matching value + # scapy + # + self.assertEqual(ord(data[0]), 1) + self.assertEqual(ord(data[1]), 4) + self.assertEqual(ord(data[2]), 0) + self.assertEqual(ord(data[3]), 0) + self.assertEqual(ord(data[4]), 0) + self.assertEqual(ord(data[5]), intf._sw_if_index) + + # + # next sub-option is the IP address of the client side + # interface. + # sub-option ID=5, length (of a v4 address)=4 + # + claddr = socket.inet_pton(AF_INET, ip_addr) + + self.assertEqual(ord(data[6]), 5) + self.assertEqual(ord(data[7]), 4) + self.assertEqual(data[8], claddr[0]) + self.assertEqual(data[9], claddr[1]) + self.assertEqual(data[10], claddr[2]) + self.assertEqual(data[11], claddr[3]) + + if oui != 0: + # sub-option 151 encodes the 3 byte oui + # and the 4 byte fib_id + self.assertEqual(ord(data[12]), 151) + self.assertEqual(ord(data[13]), 8) + self.assertEqual(ord(data[14]), 1) + self.assertEqual(ord(data[15]), 0) + self.assertEqual(ord(data[16]), 0) + self.assertEqual(ord(data[17]), oui) + self.assertEqual(ord(data[18]), 0) + self.assertEqual(ord(data[19]), 0) + self.assertEqual(ord(data[20]), 0) + self.assertEqual(ord(data[21]), fib_id) + + # VSS control sub-option + self.assertEqual(ord(data[22]), 152) + self.assertEqual(ord(data[23]), 0) + + found = 1 + self.assertTrue(found) + + return data + + def verify_dhcp_msg_type(self, pkt, name): + dhcp = pkt[DHCP] + found = False + for o in dhcp.options: + if type(o) is tuple: + if o[0] == "message-type" \ + and DHCPTypes[o[1]] == name: + found = True + self.assertTrue(found) + + def verify_dhcp_offer(self, pkt, intf, fib_id=0, oui=0): + ether = pkt[Ether] + self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") + self.assertEqual(ether.src, intf.local_mac) + + ip = pkt[IP] + self.assertEqual(ip.dst, "255.255.255.255") + self.assertEqual(ip.src, intf.local_ip4) + + udp = pkt[UDP] + self.assertEqual(udp.dport, DHCP4_CLIENT_PORT) + self.assertEqual(udp.sport, DHCP4_SERVER_PORT) + + self.verify_dhcp_msg_type(pkt, "offer") + data = self.validate_relay_options(pkt, intf, intf.local_ip4, + fib_id, oui) + + def verify_orig_dhcp_pkt(self, pkt, intf): + ether = pkt[Ether] + self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") + self.assertEqual(ether.src, intf.local_mac) + + ip = pkt[IP] + self.assertEqual(ip.dst, "255.255.255.255") + self.assertEqual(ip.src, "0.0.0.0") + + udp = pkt[UDP] + self.assertEqual(udp.dport, DHCP4_SERVER_PORT) + self.assertEqual(udp.sport, DHCP4_CLIENT_PORT) + + def verify_orig_dhcp_discover(self, pkt, intf, hostname, client_id=None): + self.verify_orig_dhcp_pkt(pkt, intf) + + self.verify_dhcp_msg_type(pkt, "discover") + self.verify_dhcp_has_option(pkt, "hostname", hostname) + if client_id: + self.verify_dhcp_has_option(pkt, "client_id", client_id) + bootp = pkt[BOOTP] + self.assertEqual(bootp.ciaddr, "0.0.0.0") + self.assertEqual(bootp.giaddr, "0.0.0.0") + self.assertEqual(bootp.flags, 0x8000) + + def verify_orig_dhcp_request(self, pkt, intf, hostname, ip): + self.verify_orig_dhcp_pkt(pkt, intf) + + self.verify_dhcp_msg_type(pkt, "request") + self.verify_dhcp_has_option(pkt, "hostname", hostname) + self.verify_dhcp_has_option(pkt, "requested_addr", ip) + bootp = pkt[BOOTP] + self.assertEqual(bootp.ciaddr, "0.0.0.0") + self.assertEqual(bootp.giaddr, "0.0.0.0") + self.assertEqual(bootp.flags, 0x8000) + + def verify_relayed_dhcp_discover(self, pkt, intf, src_intf=None, + fib_id=0, oui=0, + dst_mac=None, dst_ip=None): + if not dst_mac: + dst_mac = intf.remote_mac + if not dst_ip: + dst_ip = intf.remote_ip4 + + ether = pkt[Ether] + self.assertEqual(ether.dst, dst_mac) + self.assertEqual(ether.src, intf.local_mac) + + ip = pkt[IP] + self.assertEqual(ip.dst, dst_ip) + self.assertEqual(ip.src, intf.local_ip4) + + udp = pkt[UDP] + self.assertEqual(udp.dport, DHCP4_SERVER_PORT) + self.assertEqual(udp.sport, DHCP4_CLIENT_PORT) + + dhcp = pkt[DHCP] + + is_discover = False + for o in dhcp.options: + if type(o) is tuple: + if o[0] == "message-type" \ + and DHCPTypes[o[1]] == "discover": + is_discover = True + self.assertTrue(is_discover) + + data = self.validate_relay_options(pkt, src_intf, + src_intf.local_ip4, + fib_id, oui) + return data + + def verify_dhcp6_solicit(self, pkt, intf, + peer_ip, peer_mac, + fib_id=0, + oui=0, + dst_mac=None, + dst_ip=None): + if not dst_mac: + dst_mac = intf.remote_mac + if not dst_ip: + dst_ip = in6_ptop(intf.remote_ip6) + + ether = pkt[Ether] + self.assertEqual(ether.dst, dst_mac) + self.assertEqual(ether.src, intf.local_mac) + + ip = pkt[IPv6] + self.assertEqual(in6_ptop(ip.dst), dst_ip) + self.assertEqual(in6_ptop(ip.src), in6_ptop(intf.local_ip6)) + + udp = pkt[UDP] + self.assertEqual(udp.dport, DHCP6_CLIENT_PORT) + self.assertEqual(udp.sport, DHCP6_SERVER_PORT) + + relay = pkt[DHCP6_RelayForward] + self.assertEqual(in6_ptop(relay.peeraddr), in6_ptop(peer_ip)) + oid = pkt[DHCP6OptIfaceId] + cll = pkt[DHCP6OptClientLinkLayerAddr] + self.assertEqual(cll.optlen, 8) + self.assertEqual(cll.lltype, 1) + self.assertEqual(cll.clladdr, peer_mac) + + if fib_id != 0: + vss = pkt[DHCP6OptVSS] + self.assertEqual(vss.optlen, 8) + self.assertEqual(vss.type, 1) + # the OUI and FIB-id are really 3 and 4 bytes resp. + # but the tested range is small + self.assertEqual(ord(vss.data[0]), 0) + self.assertEqual(ord(vss.data[1]), 0) + self.assertEqual(ord(vss.data[2]), oui) + self.assertEqual(ord(vss.data[3]), 0) + self.assertEqual(ord(vss.data[4]), 0) + self.assertEqual(ord(vss.data[5]), 0) + self.assertEqual(ord(vss.data[6]), fib_id) + + # the relay message should be an encoded Solicit + msg = pkt[DHCP6OptRelayMsg] + sol = DHCP6_Solicit() + self.assertEqual(msg.optlen, len(str(sol))) + self.assertEqual(str(sol), (str(msg[1]))[:msg.optlen]) + + def verify_dhcp6_advert(self, pkt, intf, peer): + ether = pkt[Ether] + self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") + self.assertEqual(ether.src, intf.local_mac) + + ip = pkt[IPv6] + self.assertEqual(in6_ptop(ip.dst), in6_ptop(peer)) + self.assertEqual(in6_ptop(ip.src), in6_ptop(intf.local_ip6)) + + udp = pkt[UDP] + self.assertEqual(udp.dport, DHCP6_SERVER_PORT) + self.assertEqual(udp.sport, DHCP6_CLIENT_PORT) + + # not sure why this is not decoding + # adv = pkt[DHCP6_Advertise] + + def test_dhcp_proxy(self): + """ DHCPv4 Proxy """ + + # + # Verify no response to DHCP request without DHCP config + # + p_disc_vrf0 = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg2.remote_mac) / + IP(src="0.0.0.0", dst="255.255.255.255") / + UDP(sport=DHCP4_CLIENT_PORT, + dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'discover'), ('end')])) + pkts_disc_vrf0 = [p_disc_vrf0] + p_disc_vrf1 = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg3.remote_mac) / + IP(src="0.0.0.0", dst="255.255.255.255") / + UDP(sport=DHCP4_CLIENT_PORT, + dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'discover'), ('end')])) + pkts_disc_vrf1 = [p_disc_vrf0] + + self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0, + "DHCP with no configuration") + self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1, + "DHCP with no configuration") + + # + # Enable DHCP proxy in VRF 0 + # + server_addr = self.pg0.remote_ip4n + src_addr = self.pg0.local_ip4n + + self.vapi.dhcp_proxy_config(server_addr, + src_addr, + rx_table_id=0) + + # + # Discover packets from the client are dropped because there is no + # IP address configured on the client facing interface + # + self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0, + "Discover DHCP no relay address") + + # + # Inject a response from the server + # dropped, because there is no IP addrees on the + # client interfce to fill in the option. + # + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), ('end')])) + pkts = [p] + + self.send_and_assert_no_replies(self.pg2, pkts, + "Offer DHCP no relay address") + + # + # configure an IP address on the client facing interface + # + self.pg2.config_ip4() + + # + # Try again with a discover packet + # Rx'd packet should be to the server address and from the configured + # source address + # UDP source ports are unchanged + # we've no option 82 config so that should be absent + # + self.pg2.add_stream(pkts_disc_vrf0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + rx = rx[0] + + option_82 = self.verify_relayed_dhcp_discover(rx, self.pg0, + src_intf=self.pg2) + + # + # Create an DHCP offer reply from the server with a correctly formatted + # option 82. i.e. send back what we just captured + # The offer, sent mcast to the client, still has option 82. + # + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', option_82), + ('end')])) + pkts = [p] + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + rx = rx[0] + + self.verify_dhcp_offer(rx, self.pg2) + + # + # Bogus Option 82: + # + # 1. not our IP address = not checked by VPP? so offer is replayed + # to client + bad_ip = option_82[0:8] + chr(33) + option_82[9:] + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', bad_ip), + ('end')])) + pkts = [p] + self.send_and_assert_no_replies(self.pg0, pkts, + "DHCP offer option 82 bad address") + + # 2. Not a sw_if_index VPP knows + bad_if_index = option_82[0:2] + chr(33) + option_82[3:] + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', bad_if_index), + ('end')])) + pkts = [p] + self.send_and_assert_no_replies(self.pg0, pkts, + "DHCP offer option 82 bad if index") + + # + # Send a DHCP request in VRF 1. should be dropped. + # + self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1, + "DHCP with no configuration VRF 1") + + # + # Delete the DHCP config in VRF 0 + # Should now drop requests. + # + self.vapi.dhcp_proxy_config(server_addr, + src_addr, + rx_table_id=0, + is_add=0) + + self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0, + "DHCP config removed VRF 0") + self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1, + "DHCP config removed VRF 1") + + # + # Add DHCP config for VRF 1 + # + server_addr = self.pg1.remote_ip4n + src_addr = self.pg1.local_ip4n + self.vapi.dhcp_proxy_config(server_addr, + src_addr, + rx_table_id=1, + server_table_id=1) + + # + # Confim DHCP requests ok in VRF 1. + # - dropped on IP config on client interface + # + self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1, + "DHCP config removed VRF 1") + + # + # configure an IP address on the client facing interface + # + self.pg3.config_ip4() + + self.pg3.add_stream(pkts_disc_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + rx = rx[0] + self.verify_relayed_dhcp_discover(rx, self.pg1, src_intf=self.pg3) + + # + # Add VSS config + # table=1, fib=id=1, oui=4 + self.vapi.dhcp_proxy_set_vss(1, 1, 4) + + self.pg3.add_stream(pkts_disc_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + rx = rx[0] + self.verify_relayed_dhcp_discover(rx, self.pg1, + src_intf=self.pg3, + fib_id=1, oui=4) + + # + # Add a second DHCP server in VRF 1 + # expect clients messages to be relay to both configured servers + # + self.pg1.generate_remote_hosts(2) + server_addr2 = socket.inet_pton(AF_INET, self.pg1.remote_hosts[1].ip4) + + self.vapi.dhcp_proxy_config(server_addr2, + src_addr, + rx_table_id=1, + server_table_id=1, + is_add=1) + + # + # We'll need an ARP entry for the server to send it packets + # + arp_entry = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip4) + arp_entry.add_vpp_config() + + # + # Send a discover from the client. expect two relayed messages + # The frist packet is sent to the second server + # We're not enforcing that here, it's just the way it is. + # + self.pg3.add_stream(pkts_disc_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(2) + + option_82 = self.verify_relayed_dhcp_discover( + rx[0], self.pg1, + src_intf=self.pg3, + dst_mac=self.pg1.remote_hosts[1].mac, + dst_ip=self.pg1.remote_hosts[1].ip4, + fib_id=1, oui=4) + self.verify_relayed_dhcp_discover(rx[1], self.pg1, + src_intf=self.pg3, + fib_id=1, oui=4) + + # + # Send both packets back. Client gets both. + # + p1 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.pg1.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', option_82), + ('end')])) + p2 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_hosts[1].ip4, dst=self.pg1.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', option_82), + ('end')])) + pkts = [p1, p2] + + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg3.get_capture(2) + + self.verify_dhcp_offer(rx[0], self.pg3, fib_id=1, oui=4) + self.verify_dhcp_offer(rx[1], self.pg3, fib_id=1, oui=4) + + # + # Ensure offers from non-servers are dropeed + # + p2 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src="8.8.8.8", dst=self.pg1.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'offer'), + ('relay_agent_Information', option_82), + ('end')])) + self.send_and_assert_no_replies(self.pg1, p2, + "DHCP offer from non-server") + + # + # Ensure only the discover is sent to multiple servers + # + p_req_vrf1 = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg3.remote_mac) / + IP(src="0.0.0.0", dst="255.255.255.255") / + UDP(sport=DHCP4_CLIENT_PORT, + dport=DHCP4_SERVER_PORT) / + BOOTP(op=1) / + DHCP(options=[('message-type', 'request'), + ('end')])) + + self.pg3.add_stream(p_req_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + # + # Remove the second DHCP server + # + self.vapi.dhcp_proxy_config(server_addr2, + src_addr, + rx_table_id=1, + server_table_id=1, + is_add=0) + + # + # Test we can still relay with the first + # + self.pg3.add_stream(pkts_disc_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + rx = rx[0] + self.verify_relayed_dhcp_discover(rx, self.pg1, + src_intf=self.pg3, + fib_id=1, oui=4) + + # + # Remove the VSS config + # relayed DHCP has default vlaues in the option. + # + self.vapi.dhcp_proxy_set_vss(1, 1, 4, is_add=0) + + self.pg3.add_stream(pkts_disc_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + rx = rx[0] + self.verify_relayed_dhcp_discover(rx, self.pg1, src_intf=self.pg3) + + # + # remove DHCP config to cleanup + # + self.vapi.dhcp_proxy_config(server_addr, + src_addr, + rx_table_id=1, + server_table_id=1, + is_add=0) + + self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0, + "DHCP cleanup VRF 0") + self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1, + "DHCP cleanup VRF 1") + self.pg2.unconfig_ip4() + self.pg3.unconfig_ip4() + + def test_dhcp6_proxy(self): + """ DHCPv6 Proxy""" + # + # Verify no response to DHCP request without DHCP config + # + dhcp_solicit_dst = "ff02::1:2" + dhcp_solicit_src_vrf0 = mk_ll_addr(self.pg2.remote_mac) + dhcp_solicit_src_vrf1 = mk_ll_addr(self.pg3.remote_mac) + server_addr_vrf0 = self.pg0.remote_ip6n + src_addr_vrf0 = self.pg0.local_ip6n + server_addr_vrf1 = self.pg1.remote_ip6n + src_addr_vrf1 = self.pg1.local_ip6n + + dmac = in6_getnsmac(inet_pton(socket.AF_INET6, dhcp_solicit_dst)) + p_solicit_vrf0 = (Ether(dst=dmac, src=self.pg2.remote_mac) / + IPv6(src=dhcp_solicit_src_vrf0, + dst=dhcp_solicit_dst) / + UDP(sport=DHCP6_SERVER_PORT, + dport=DHCP6_CLIENT_PORT) / + DHCP6_Solicit()) + p_solicit_vrf1 = (Ether(dst=dmac, src=self.pg3.remote_mac) / + IPv6(src=dhcp_solicit_src_vrf1, + dst=dhcp_solicit_dst) / + UDP(sport=DHCP6_SERVER_PORT, + dport=DHCP6_CLIENT_PORT) / + DHCP6_Solicit()) + + self.send_and_assert_no_replies(self.pg2, p_solicit_vrf0, + "DHCP with no configuration") + self.send_and_assert_no_replies(self.pg3, p_solicit_vrf1, + "DHCP with no configuration") + + # + # DHCPv6 config in VRF 0. + # Packets still dropped because the client facing interface has no + # IPv6 config + # + self.vapi.dhcp_proxy_config(server_addr_vrf0, + src_addr_vrf0, + rx_table_id=0, + server_table_id=0, + is_ipv6=1) + + self.send_and_assert_no_replies(self.pg2, p_solicit_vrf0, + "DHCP with no configuration") + self.send_and_assert_no_replies(self.pg3, p_solicit_vrf1, + "DHCP with no configuration") + + # + # configure an IP address on the client facing interface + # + self.pg2.config_ip6() + + # + # Now the DHCP requests are relayed to the server + # + self.pg2.add_stream(p_solicit_vrf0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + + self.verify_dhcp6_solicit(rx[0], self.pg0, + dhcp_solicit_src_vrf0, + self.pg2.remote_mac) + + # + # Exception cases for rejected relay responses + # + + # 1 - not a relay reply + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_Advertise()) + self.send_and_assert_no_replies(self.pg2, p_adv_vrf0, + "DHCP6 not a relay reply") + + # 2 - no relay message option + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply() / + DHCP6_Advertise()) + self.send_and_assert_no_replies(self.pg2, p_adv_vrf0, + "DHCP not a relay message") + + # 3 - no circuit ID + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply() / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise()) + self.send_and_assert_no_replies(self.pg2, p_adv_vrf0, + "DHCP6 no circuit ID") + # 4 - wrong circuit ID + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply() / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x05') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise()) + self.send_and_assert_no_replies(self.pg2, p_adv_vrf0, + "DHCP6 wrong circuit ID") + + # + # Send the relay response (the advertisement) + # - no peer address + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply() / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x03') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + pkts_adv_vrf0 = [p_adv_vrf0] + + self.pg0.add_stream(pkts_adv_vrf0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + + self.verify_dhcp6_advert(rx[0], self.pg2, "::") + + # + # Send the relay response (the advertisement) + # - with peer address + p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf0) / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x03') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + pkts_adv_vrf0 = [p_adv_vrf0] + + self.pg0.add_stream(pkts_adv_vrf0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + + self.verify_dhcp6_advert(rx[0], self.pg2, dhcp_solicit_src_vrf0) + + # + # Add all the config for VRF 1 + # + self.vapi.dhcp_proxy_config(server_addr_vrf1, + src_addr_vrf1, + rx_table_id=1, + server_table_id=1, + is_ipv6=1) + self.pg3.config_ip6() + + # + # VRF 1 solicit + # + self.pg3.add_stream(p_solicit_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_dhcp6_solicit(rx[0], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac) + + # + # VRF 1 Advert + # + p_adv_vrf1 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IPv6(dst=self.pg1.local_ip6, src=self.pg1.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf1) / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x04') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + pkts_adv_vrf1 = [p_adv_vrf1] + + self.pg1.add_stream(pkts_adv_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg3.get_capture(1) + + self.verify_dhcp6_advert(rx[0], self.pg3, dhcp_solicit_src_vrf1) + + # + # Add VSS config + # table=1, fib=id=1, oui=4 + self.vapi.dhcp_proxy_set_vss(1, 1, 4, is_ip6=1) + + self.pg3.add_stream(p_solicit_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_dhcp6_solicit(rx[0], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac, + fib_id=1, + oui=4) + + # + # Remove the VSS config + # relayed DHCP has default vlaues in the option. + # + self.vapi.dhcp_proxy_set_vss(1, 1, 4, is_ip6=1, is_add=0) + + self.pg3.add_stream(p_solicit_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_dhcp6_solicit(rx[0], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac) + + # + # Add a second DHCP server in VRF 1 + # expect clients messages to be relay to both configured servers + # + self.pg1.generate_remote_hosts(2) + server_addr2 = socket.inet_pton(AF_INET6, self.pg1.remote_hosts[1].ip6) + + self.vapi.dhcp_proxy_config(server_addr2, + src_addr_vrf1, + rx_table_id=1, + server_table_id=1, + is_ipv6=1) + + # + # We'll need an ND entry for the server to send it packets + # + nd_entry = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip6, + af=AF_INET6) + nd_entry.add_vpp_config() + + # + # Send a discover from the client. expect two relayed messages + # The frist packet is sent to the second server + # We're not enforcing that here, it's just the way it is. + # + self.pg3.add_stream(p_solicit_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(2) + + self.verify_dhcp6_solicit(rx[0], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac) + self.verify_dhcp6_solicit(rx[1], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac, + dst_mac=self.pg1.remote_hosts[1].mac, + dst_ip=self.pg1.remote_hosts[1].ip6) + + # + # Send both packets back. Client gets both. + # + p1 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IPv6(dst=self.pg1.local_ip6, src=self.pg1.remote_ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf1) / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x04') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + p2 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_hosts[1].mac) / + IPv6(dst=self.pg1.local_ip6, src=self.pg1._remote_hosts[1].ip6) / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf1) / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x04') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + + pkts = [p1, p2] + + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg3.get_capture(2) + + self.verify_dhcp6_advert(rx[0], self.pg3, dhcp_solicit_src_vrf1) + self.verify_dhcp6_advert(rx[1], self.pg3, dhcp_solicit_src_vrf1) + + # + # Ensure only solicit messages are duplicated + # + p_request_vrf1 = (Ether(dst=dmac, src=self.pg3.remote_mac) / + IPv6(src=dhcp_solicit_src_vrf1, + dst=dhcp_solicit_dst) / + UDP(sport=DHCP6_SERVER_PORT, + dport=DHCP6_CLIENT_PORT) / + DHCP6_Request()) + + self.pg3.add_stream(p_request_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + # + # Test we drop DHCP packets from addresses that are not configured as + # DHCP servers + # + p2 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_hosts[1].mac) / + IPv6(dst=self.pg1.local_ip6, src="3001::1") / + UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) / + DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf1) / + DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x04') / + DHCP6OptRelayMsg(optlen=0) / + DHCP6_Advertise(trid=1) / + DHCP6OptStatusCode(statuscode=0)) + self.send_and_assert_no_replies(self.pg1, p2, + "DHCP6 not from server") + + # + # Remove the second DHCP server + # + self.vapi.dhcp_proxy_config(server_addr2, + src_addr_vrf1, + rx_table_id=1, + server_table_id=1, + is_ipv6=1, + is_add=0) + + # + # Test we can still relay with the first + # + self.pg3.add_stream(p_solicit_vrf1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_dhcp6_solicit(rx[0], self.pg1, + dhcp_solicit_src_vrf1, + self.pg3.remote_mac) + + # + # Cleanup + # + self.vapi.dhcp_proxy_config(server_addr_vrf1, + src_addr_vrf1, + rx_table_id=1, + server_table_id=1, + is_ipv6=1, + is_add=0) + self.vapi.dhcp_proxy_config(server_addr_vrf0, + src_addr_vrf0, + rx_table_id=0, + server_table_id=0, + is_ipv6=1, + is_add=0) + + # duplicate delete + self.vapi.dhcp_proxy_config(server_addr_vrf0, + src_addr_vrf0, + rx_table_id=0, + server_table_id=0, + is_ipv6=1, + is_add=0) + self.pg2.unconfig_ip6() + self.pg3.unconfig_ip6() + + def test_dhcp_client(self): + """ DHCP Client""" + + hostname = 'universal-dp' + + self.pg_enable_capture(self.pg_interfaces) + + # + # Configure DHCP client on PG2 and capture the discover sent + # + self.vapi.dhcp_client(self.pg2.sw_if_index, hostname) + + rx = self.pg2.get_capture(1) + + self.verify_orig_dhcp_discover(rx[0], self.pg2, hostname) + + # + # Sned back on offer, expect the request + # + p_offer = (Ether(dst=self.pg2.local_mac, src=self.pg2.remote_mac) / + IP(src=self.pg2.remote_ip4, dst="255.255.255.255") / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_CLIENT_PORT) / + BOOTP(op=1, yiaddr=self.pg2.local_ip4) / + DHCP(options=[('message-type', 'offer'), + ('server_id', self.pg2.remote_ip4), + ('end')])) + + self.pg2.add_stream(p_offer) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_orig_dhcp_request(rx[0], self.pg2, hostname, + self.pg2.local_ip4) + + # + # Send an acknowloedgement + # + p_ack = (Ether(dst=self.pg2.local_mac, src=self.pg2.remote_mac) / + IP(src=self.pg2.remote_ip4, dst="255.255.255.255") / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_CLIENT_PORT) / + BOOTP(op=1, yiaddr=self.pg2.local_ip4) / + DHCP(options=[('message-type', 'ack'), + ('subnet_mask', "255.255.255.0"), + ('router', self.pg2.remote_ip4), + ('server_id', self.pg2.remote_ip4), + ('lease_time', 43200), + ('end')])) + + self.pg2.add_stream(p_ack) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # + # We'll get an ARP request for the router address + # + rx = self.pg2.get_capture(1) + + self.assertEqual(rx[0][ARP].pdst, self.pg2.remote_ip4) + self.pg_enable_capture(self.pg_interfaces) + + # + # At the end of this procedure there should be a connected route + # in the FIB + # + self.assertTrue(find_route(self, self.pg2.local_ip4, 24)) + self.assertTrue(find_route(self, self.pg2.local_ip4, 32)) + + # remove the left over ARP entry + self.vapi.ip_neighbor_add_del(self.pg2.sw_if_index, + mactobinary(self.pg2.remote_mac), + self.pg2.remote_ip4, + is_add=0) + # + # remove the DHCP config + # + self.vapi.dhcp_client(self.pg2.sw_if_index, hostname, is_add=0) + + # + # and now the route should be gone + # + self.assertFalse(find_route(self, self.pg2.local_ip4, 32)) + self.assertFalse(find_route(self, self.pg2.local_ip4, 24)) + + # + # Start the procedure again. this time have VPP send the client-ID + # + self.pg2.admin_down() + self.sleep(1) + self.pg2.admin_up() + self.vapi.dhcp_client(self.pg2.sw_if_index, hostname, + client_id=self.pg2.local_mac) + + rx = self.pg2.get_capture(1) + + self.verify_orig_dhcp_discover(rx[0], self.pg2, hostname, + self.pg2.local_mac) + + self.pg2.add_stream(p_offer) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_orig_dhcp_request(rx[0], self.pg2, hostname, + self.pg2.local_ip4) + + # + # unicast the ack to the offered address + # + p_ack = (Ether(dst=self.pg2.local_mac, src=self.pg2.remote_mac) / + IP(src=self.pg2.remote_ip4, dst=self.pg2.local_ip4) / + UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_CLIENT_PORT) / + BOOTP(op=1, yiaddr=self.pg2.local_ip4) / + DHCP(options=[('message-type', 'ack'), + ('subnet_mask', "255.255.255.0"), + ('router', self.pg2.remote_ip4), + ('server_id', self.pg2.remote_ip4), + ('lease_time', 43200), + ('end')])) + + self.pg2.add_stream(p_ack) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # + # At the end of this procedure there should be a connected route + # in the FIB + # + self.assertTrue(find_route(self, self.pg2.local_ip4, 32)) + self.assertTrue(find_route(self, self.pg2.local_ip4, 24)) + + # + # remove the DHCP config + # + self.vapi.dhcp_client(self.pg2.sw_if_index, hostname, is_add=0) + + self.assertFalse(find_route(self, self.pg2.local_ip4, 32)) + self.assertFalse(find_route(self, self.pg2.local_ip4, 24)) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_fib.py b/test/test_fib.py new file mode 100644 index 00000000..1e28e8f8 --- /dev/null +++ b/test/test_fib.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import unittest + +from framework import VppTestCase, VppTestRunner + + +class TestFIB(VppTestCase): + """ FIB Test Case """ + + @classmethod + def setUpClass(cls): + super(TestFIB, cls).setUpClass() + + def setUp(self): + super(TestFIB, self).setUp() + + def tearDown(self): + super(TestFIB, self).tearDown() + + def test_fib(self): + """ FIB Unit Tests """ + error = self.vapi.cli("test fib") + + if error: + self.logger.critical(error) + self.assertEqual(error.find("Failed"), -1) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_flowprobe.py b/test/test_flowprobe.py new file mode 100644 index 00000000..df6b4230 --- /dev/null +++ b/test/test_flowprobe.py @@ -0,0 +1,1021 @@ +#!/usr/bin/env python +import random +import socket +import unittest +import time +import re + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, TCP, UDP +from scapy.layers.inet6 import IPv6 + +from framework import VppTestCase, VppTestRunner, running_extended_tests +from vpp_object import VppObject +from vpp_pg_interface import CaptureTimeoutError +from util import ppp +from ipfix import IPFIX, Set, Template, Data, IPFIXDecoder +from vpp_ip_route import VppIpRoute, VppRoutePath + + +class VppCFLOW(VppObject): + """CFLOW object for IPFIX exporter and Flowprobe feature""" + + def __init__(self, test, intf='pg2', active=0, passive=0, timeout=100, + mtu=1024, datapath='l2', layer='l2 l3 l4'): + self._test = test + self._intf = intf + self._active = active + if passive == 0 or passive < active: + self._passive = active+1 + else: + self._passive = passive + self._datapath = datapath # l2 ip4 ip6 + self._collect = layer # l2 l3 l4 + self._timeout = timeout + self._mtu = mtu + self._configured = False + + def add_vpp_config(self): + self.enable_exporter() + self._test.vapi.ppcli("flowprobe params record %s active %s " + "passive %s" % (self._collect, self._active, + self._passive)) + self.enable_flowprobe_feature() + self._test.vapi.cli("ipfix flush") + self._configured = True + + def remove_vpp_config(self): + self.disable_exporter() + self.disable_flowprobe_feature() + self._test.vapi.cli("ipfix flush") + self._configured = False + + def enable_exporter(self): + self._test.vapi.set_ipfix_exporter( + collector_address=self._test.pg0.remote_ip4n, + src_address=self._test.pg0.local_ip4n, + path_mtu=self._mtu, + template_interval=self._timeout) + + def enable_flowprobe_feature(self): + self._test.vapi.ppcli("flowprobe feature add-del %s %s" % + (self._intf, self._datapath)) + + def disable_exporter(self): + self._test.vapi.cli("set ipfix exporter collector 0.0.0.0") + + def disable_flowprobe_feature(self): + self._test.vapi.cli("flowprobe feature add-del %s %s disable" % + (self._intf, self._datapath)) + + def object_id(self): + return "ipfix-collector-%s" % (self._src, self.dst) + + def query_vpp_config(self): + return self._configured + + def verify_templates(self, decoder=None, timeout=1, count=3): + templates = [] + p = self._test.wait_for_cflow_packet(self._test.collector, 2, timeout) + self._test.assertTrue(p.haslayer(IPFIX)) + if decoder is not None and p.haslayer(Template): + templates.append(p[Template].templateID) + decoder.add_template(p.getlayer(Template)) + if count > 1: + p = self._test.wait_for_cflow_packet(self._test.collector, 2) + self._test.assertTrue(p.haslayer(IPFIX)) + if decoder is not None and p.haslayer(Template): + templates.append(p[Template].templateID) + decoder.add_template(p.getlayer(Template)) + if count > 2: + p = self._test.wait_for_cflow_packet(self._test.collector, 2) + self._test.assertTrue(p.haslayer(IPFIX)) + if decoder is not None and p.haslayer(Template): + templates.append(p[Template].templateID) + decoder.add_template(p.getlayer(Template)) + return templates + + +class MethodHolder(VppTestCase): + """ Flow-per-packet plugin: test L2, IP4, IP6 reporting """ + + # Test variables + debug_print = False + max_number_of_packets = 10 + pkts = [] + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(MethodHolder, cls).setUpClass() + try: + # Create pg interfaces + cls.create_pg_interfaces(range(9)) + + # Packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + # Create BD with MAC learning and unknown unicast flooding disabled + # and put interfaces to this BD + cls.vapi.bridge_domain_add_del(bd_id=1, uu_flood=1, learn=1) + cls.vapi.sw_interface_set_l2_bridge(cls.pg1._sw_if_index, bd_id=1) + cls.vapi.sw_interface_set_l2_bridge(cls.pg2._sw_if_index, bd_id=1) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + cls.pg0.config_ip4() + cls.pg0.configure_ipv4_neighbors() + cls.collector = cls.pg0 + + cls.pg1.config_ip4() + cls.pg1.resolve_arp() + cls.pg2.config_ip4() + cls.pg2.resolve_arp() + cls.pg3.config_ip4() + cls.pg3.resolve_arp() + cls.pg4.config_ip4() + cls.pg4.resolve_arp() + cls.pg7.config_ip4() + cls.pg8.config_ip4() + cls.pg8.configure_ipv4_neighbors() + + cls.pg5.config_ip6() + cls.pg5.resolve_ndp() + cls.pg5.disable_ipv6_ra() + cls.pg6.config_ip6() + cls.pg6.resolve_ndp() + cls.pg6.disable_ipv6_ra() + except Exception: + super(MethodHolder, cls).tearDownClass() + raise + + def create_stream(self, src_if=None, dst_if=None, packets=None, + size=None, ip_ver='v4'): + """Create a packet stream to tickle the plugin + + :param VppInterface src_if: Source interface for packet stream + :param VppInterface src_if: Dst interface for packet stream + """ + if src_if is None: + src_if = self.pg1 + if dst_if is None: + dst_if = self.pg2 + self.pkts = [] + if packets is None: + packets = random.randint(1, self.max_number_of_packets) + pkt_size = size + for p in range(0, packets): + if size is None: + pkt_size = random.choice(self.pg_if_packet_sizes) + info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(info) + p = Ether(src=src_if.remote_mac, dst=src_if.local_mac) + if ip_ver == 'v4': + p /= IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) + else: + p /= IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6) + p /= UDP(sport=1234, dport=4321) + p /= Raw(payload) + info.data = p.copy() + self.extend_packet(p, pkt_size) + self.pkts.append(p) + + def verify_cflow_data(self, decoder, capture, cflow): + octets = 0 + packets = 0 + for p in capture: + octets += p[IP].len + packets += 1 + if cflow.haslayer(Data): + data = decoder.decode_data_set(cflow.getlayer(Set)) + for record in data: + self.assertEqual(int(record[1].encode('hex'), 16), octets) + self.assertEqual(int(record[2].encode('hex'), 16), packets) + + def send_packets(self, src_if=None, dst_if=None): + if src_if is None: + src_if = self.pg1 + if dst_if is None: + dst_if = self.pg2 + self.pg_enable_capture([dst_if]) + src_if.add_stream(self.pkts) + self.pg_start() + return dst_if.get_capture(len(self.pkts)) + + def verify_cflow_data_detail(self, decoder, capture, cflow, + data_set={1: 'octets', 2: 'packets'}, + ip_ver='v4'): + if self.debug_print: + print capture[0].show() + if cflow.haslayer(Data): + data = decoder.decode_data_set(cflow.getlayer(Set)) + if self.debug_print: + print data + if ip_ver == 'v4': + ip_layer = capture[0][IP] + else: + ip_layer = capture[0][IPv6] + if data_set is not None: + for record in data: + # skip flow if in/out gress interface is 0 + if int(record[10].encode('hex'), 16) == 0: + continue + if int(record[14].encode('hex'), 16) == 0: + continue + + for field in data_set: + if field not in record.keys(): + continue + value = data_set[field] + if value == 'octets': + value = ip_layer.len + if ip_ver == 'v6': + value += 40 # ??? is this correct + elif value == 'packets': + value = 1 + elif value == 'src_ip': + if ip_ver == 'v4': + ip = socket.inet_pton(socket.AF_INET, + ip_layer.src) + else: + ip = socket.inet_pton(socket.AF_INET6, + ip_layer.src) + value = int(ip.encode('hex'), 16) + elif value == 'dst_ip': + if ip_ver == 'v4': + ip = socket.inet_pton(socket.AF_INET, + ip_layer.dst) + else: + ip = socket.inet_pton(socket.AF_INET6, + ip_layer.dst) + value = int(ip.encode('hex'), 16) + elif value == 'sport': + value = int(capture[0][UDP].sport) + elif value == 'dport': + value = int(capture[0][UDP].dport) + self.assertEqual(int(record[field].encode('hex'), 16), + value) + + def verify_cflow_data_notimer(self, decoder, capture, cflows): + idx = 0 + for cflow in cflows: + if cflow.haslayer(Data): + data = decoder.decode_data_set(cflow.getlayer(Set)) + else: + raise Exception("No CFLOW data") + + for rec in data: + p = capture[idx] + idx += 1 + self.assertEqual(p[IP].len, int(rec[1].encode('hex'), 16)) + self.assertEqual(1, int(rec[2].encode('hex'), 16)) + self.assertEqual(len(capture), idx) + + def wait_for_cflow_packet(self, collector_intf, set_id=2, timeout=1, + expected=True): + """ wait for CFLOW packet and verify its correctness + + :param timeout: how long to wait + + :returns: tuple (packet, time spent waiting for packet) + """ + self.logger.info("IPFIX: Waiting for CFLOW packet") + deadline = time.time() + timeout + counter = 0 + # self.logger.debug(self.vapi.ppcli("show flow table")) + while True: + counter += 1 + # sanity check + self.assert_in_range(counter, 0, 100, "number of packets ignored") + time_left = deadline - time.time() + try: + if time_left < 0 and expected: + # self.logger.debug(self.vapi.ppcli("show flow table")) + raise CaptureTimeoutError( + "Packet did not arrive within timeout") + p = collector_intf.wait_for_packet(timeout=time_left) + except CaptureTimeoutError: + if expected: + # self.logger.debug(self.vapi.ppcli("show flow table")) + raise CaptureTimeoutError( + "Packet did not arrive within timeout") + else: + return + if not expected: + raise CaptureTimeoutError("Packet arrived even not expected") + self.assertEqual(p[Set].setID, set_id) + # self.logger.debug(self.vapi.ppcli("show flow table")) + self.logger.debug(ppp("IPFIX: Got packet:", p)) + break + return p + + +class Flowprobe(MethodHolder): + """Template verification, timer tests""" + + def test_0001(self): + """ timer less than template timeout""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, active=2) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream(packets=1) + self.send_packets() + capture = self.pg2.get_capture(1) + + # make sure the one packet we expect actually showed up + cflow = self.wait_for_cflow_packet(self.collector, templates[1], 15) + self.verify_cflow_data(ipfix_decoder, capture, cflow) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + def test_0002(self): + """ timer greater than template timeout""" + self.logger.info("FFP_TEST_START_0002") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, timeout=3, active=4) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + ipfix.verify_templates() + + self.create_stream(packets=2) + self.send_packets() + capture = self.pg2.get_capture(2) + + # next set of template packet should arrive after 20 seconds + # template packet should arrive within 20 s + templates = ipfix.verify_templates(ipfix_decoder, timeout=5) + + # make sure the one packet we expect actually showed up + cflow = self.wait_for_cflow_packet(self.collector, templates[1], 15) + self.verify_cflow_data(ipfix_decoder, capture, cflow) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_cflow_packet(self): + """verify cflow packet fields""" + self.logger.info("FFP_TEST_START_0000") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg8', datapath="ip4", + layer='l2 l3 l4', active=2) + ipfix.add_vpp_config() + + route_9001 = VppIpRoute(self, "9.0.0.0", 24, + [VppRoutePath(self.pg8._remote_hosts[0].ip4, + self.pg8.sw_if_index)]) + route_9001.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.pkts = [(Ether(dst=self.pg7.local_mac, + src=self.pg7.remote_mac) / + IP(src=self.pg7.remote_ip4, dst="9.0.0.100") / + TCP(sport=1234, dport=4321, flags=80) / + Raw('\xa5' * 100))] + + nowUTC = int(time.time()) + nowUNIX = nowUTC+2208988800 + self.send_packets(src_if=self.pg7, dst_if=self.pg8) + + cflow = self.wait_for_cflow_packet(self.collector, templates[0], 10) + self.collector.get_capture(2) + + if cflow[0].haslayer(IPFIX): + self.assertEqual(cflow[IPFIX].version, 10) + self.assertEqual(cflow[IPFIX].observationDomainID, 1) + self.assertEqual(cflow[IPFIX].sequenceNumber, 0) + self.assertAlmostEqual(cflow[IPFIX].exportTime, nowUTC, delta=5) + if cflow.haslayer(Data): + record = ipfix_decoder.decode_data_set(cflow[0].getlayer(Set))[0] + # ingress interface + self.assertEqual(int(record[10].encode('hex'), 16), 8) + # egress interface + self.assertEqual(int(record[14].encode('hex'), 16), 9) + # packets + self.assertEqual(int(record[2].encode('hex'), 16), 1) + # src mac + self.assertEqual(':'.join(re.findall('..', record[56].encode( + 'hex'))), self.pg8.local_mac) + # dst mac + self.assertEqual(':'.join(re.findall('..', record[80].encode( + 'hex'))), self.pg8.remote_mac) + flowTimestamp = int(record[156].encode('hex'), 16) >> 32 + # flow start timestamp + self.assertAlmostEqual(flowTimestamp, nowUNIX, delta=1) + flowTimestamp = int(record[157].encode('hex'), 16) >> 32 + # flow end timestamp + self.assertAlmostEqual(flowTimestamp, nowUNIX, delta=1) + # ethernet type + self.assertEqual(int(record[256].encode('hex'), 16), 8) + # src ip + self.assertEqual('.'.join(re.findall('..', record[8].encode( + 'hex'))), + '.'.join('{:02x}'.format(int(n)) for n in + self.pg7.remote_ip4.split('.'))) + # dst ip + self.assertEqual('.'.join(re.findall('..', record[12].encode( + 'hex'))), + '.'.join('{:02x}'.format(int(n)) for n in + "9.0.0.100".split('.'))) + # protocol (TCP) + self.assertEqual(int(record[4].encode('hex'), 16), 6) + # src port + self.assertEqual(int(record[7].encode('hex'), 16), 1234) + # dst port + self.assertEqual(int(record[11].encode('hex'), 16), 4321) + # tcp flags + self.assertEqual(int(record[6].encode('hex'), 16), 80) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0000") + + +class Datapath(MethodHolder): + """collect information on Ethernet, IP4 and IP6 datapath (no timers)""" + + def test_templatesL2(self): + """ verify template on L2 datapath""" + self.logger.info("FFP_TEST_START_0000") + self.pg_enable_capture(self.pg_interfaces) + + ipfix = VppCFLOW(test=self, layer='l2') + ipfix.add_vpp_config() + + # template packet should arrive immediately + self.vapi.cli("ipfix flush") + ipfix.verify_templates(timeout=3, count=1) + self.collector.get_capture(1) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0000") + + def test_L2onL2(self): + """ L2 data on L2 datapath""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, layer='l2') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(packets=1) + capture = self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 256: 8}) + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + def test_L3onL2(self): + """ L3 data on L2 datapath""" + self.logger.info("FFP_TEST_START_0002") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, layer='l3') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=2) + + self.create_stream(packets=1) + capture = self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 4: 17, + 8: 'src_ip', 12: 'dst_ip'}) + + self.collector.get_capture(3) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_L4onL2(self): + """ L4 data on L2 datapath""" + self.logger.info("FFP_TEST_START_0003") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, layer='l4') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=2) + + self.create_stream(packets=1) + capture = self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 7: 'sport', 11: 'dport'}) + + self.collector.get_capture(3) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0003") + + def test_templatesIp4(self): + """ verify templates on IP4 datapath""" + self.logger.info("FFP_TEST_START_0000") + + self.pg_enable_capture(self.pg_interfaces) + + ipfix = VppCFLOW(test=self, datapath='ip4') + ipfix.add_vpp_config() + + # template packet should arrive immediately + self.vapi.cli("ipfix flush") + ipfix.verify_templates(timeout=3, count=1) + self.collector.get_capture(1) + + ipfix.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0000") + + def test_L2onIP4(self): + """ L2 data on IP4 datapath""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg4', layer='l2', datapath='ip4') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg3, dst_if=self.pg4, packets=1) + capture = self.send_packets(src_if=self.pg3, dst_if=self.pg4) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 256: 8}) + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + def test_L3onIP4(self): + """ L3 data on IP4 datapath""" + self.logger.info("FFP_TEST_START_0002") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg4', layer='l3', datapath='ip4') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg3, dst_if=self.pg4, packets=1) + capture = self.send_packets(src_if=self.pg3, dst_if=self.pg4) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {1: 'octets', 2: 'packets', + 8: 'src_ip', 12: 'dst_ip'}) + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_L4onIP4(self): + """ L4 data on IP4 datapath""" + self.logger.info("FFP_TEST_START_0003") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg4', layer='l4', datapath='ip4') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg3, dst_if=self.pg4, packets=1) + capture = self.send_packets(src_if=self.pg3, dst_if=self.pg4) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 7: 'sport', 11: 'dport'}) + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0003") + + def test_templatesIP6(self): + """ verify templates on IP6 datapath""" + self.logger.info("FFP_TEST_START_0000") + self.pg_enable_capture(self.pg_interfaces) + + ipfix = VppCFLOW(test=self, datapath='ip6') + ipfix.add_vpp_config() + + # template packet should arrive immediately + ipfix.verify_templates(count=1) + self.collector.get_capture(1) + + ipfix.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0000") + + def test_L2onIP6(self): + """ L2 data on IP6 datapath""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg6', layer='l2', datapath='ip6') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg5, dst_if=self.pg6, packets=1, + ip_ver='IPv6') + capture = self.send_packets(src_if=self.pg5, dst_if=self.pg6) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 256: 56710}, + ip_ver='v6') + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + def test_L3onIP6(self): + """ L3 data on IP6 datapath""" + self.logger.info("FFP_TEST_START_0002") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg6', layer='l3', datapath='ip6') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg5, dst_if=self.pg6, packets=1, + ip_ver='IPv6') + capture = self.send_packets(src_if=self.pg5, dst_if=self.pg6) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', + 27: 'src_ip', 28: 'dst_ip'}, + ip_ver='v6') + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_L4onIP6(self): + """ L4 data on IP6 datapath""" + self.logger.info("FFP_TEST_START_0003") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, intf='pg6', layer='l4', datapath='ip6') + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder, count=1) + + self.create_stream(src_if=self.pg5, dst_if=self.pg6, packets=1, + ip_ver='IPv6') + capture = self.send_packets(src_if=self.pg5, dst_if=self.pg6) + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[0]) + self.verify_cflow_data_detail(ipfix_decoder, capture, cflow, + {2: 'packets', 7: 'sport', 11: 'dport'}, + ip_ver='v6') + + # expected two templates and one cflow packet + self.collector.get_capture(2) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0003") + + def test_0001(self): + """ no timers, one CFLOW packet, 9 Flows inside""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream(packets=9) + capture = self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + cflow = self.wait_for_cflow_packet(self.collector, templates[1]) + self.verify_cflow_data_notimer(ipfix_decoder, capture, [cflow]) + self.collector.get_capture(4) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + def test_0002(self): + """ no timers, two CFLOW packets (mtu=256), 3 Flows in each""" + self.logger.info("FFP_TEST_START_0002") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self, mtu=256) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + self.vapi.cli("ipfix flush") + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream(packets=6) + capture = self.send_packets() + + # make sure the one packet we expect actually showed up + cflows = [] + self.vapi.cli("ipfix flush") + cflows.append(self.wait_for_cflow_packet(self.collector, + templates[1])) + cflows.append(self.wait_for_cflow_packet(self.collector, + templates[1])) + self.verify_cflow_data_notimer(ipfix_decoder, capture, cflows) + self.collector.get_capture(5) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class DisableIPFIX(MethodHolder): + """Disable IPFIX""" + + def test_0001(self): + """ disable IPFIX after first packets""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream() + self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1]) + self.collector.get_capture(4) + + # disble IPFIX + ipfix.disable_exporter() + self.pg_enable_capture([self.collector]) + + self.send_packets() + + # make sure no one packet arrived in 1 minute + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], + expected=False) + self.collector.get_capture(0) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class ReenableIPFIX(MethodHolder): + """Re-enable IPFIX""" + + def test_0011(self): + """ disable IPFIX after first packets and re-enable after few packets + """ + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream(packets=5) + self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1]) + self.collector.get_capture(4) + + # disble IPFIX + ipfix.disable_exporter() + self.vapi.cli("ipfix flush") + self.pg_enable_capture([self.collector]) + + self.send_packets() + + # make sure no one packet arrived in active timer span + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], + expected=False) + self.collector.get_capture(0) + self.pg2.get_capture(5) + + # enable IPFIX + ipfix.enable_exporter() + + capture = self.collector.get_capture(4) + nr_templates = 0 + nr_data = 0 + for p in capture: + self.assertTrue(p.haslayer(IPFIX)) + if p.haslayer(Template): + nr_templates += 1 + self.assertTrue(nr_templates, 3) + for p in capture: + self.assertTrue(p.haslayer(IPFIX)) + if p.haslayer(Data): + nr_data += 1 + self.assertTrue(nr_templates, 1) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class DisableFP(MethodHolder): + """Disable Flowprobe feature""" + + def test_0001(self): + """ disable flowprobe feature after first packets""" + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + ipfix = VppCFLOW(test=self) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + templates = ipfix.verify_templates(ipfix_decoder) + + self.create_stream() + self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1]) + self.collector.get_capture(4) + + # disble IPFIX + ipfix.disable_flowprobe_feature() + self.pg_enable_capture([self.collector]) + + self.send_packets() + + # make sure no one packet arrived in active timer span + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], + expected=False) + self.collector.get_capture(0) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class ReenableFP(MethodHolder): + """Re-enable Flowprobe feature""" + + def test_0001(self): + """ disable flowprobe feature after first packets and re-enable + after few packets """ + self.logger.info("FFP_TEST_START_0001") + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + ipfix = VppCFLOW(test=self) + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + # template packet should arrive immediately + self.vapi.cli("ipfix flush") + templates = ipfix.verify_templates(ipfix_decoder, timeout=3) + + self.create_stream() + self.send_packets() + + # make sure the one packet we expect actually showed up + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], 5) + self.collector.get_capture(4) + + # disble FPP feature + ipfix.disable_flowprobe_feature() + self.pg_enable_capture([self.collector]) + + self.send_packets() + + # make sure no one packet arrived in active timer span + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], 5, + expected=False) + self.collector.get_capture(0) + + # enable FPP feature + ipfix.enable_flowprobe_feature() + self.vapi.cli("ipfix flush") + templates = ipfix.verify_templates(ipfix_decoder, timeout=3) + + self.send_packets() + + # make sure the next packets (templates and data) we expect actually + # showed up + self.vapi.cli("ipfix flush") + self.wait_for_cflow_packet(self.collector, templates[1], 5) + self.collector.get_capture(4) + + ipfix.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0001") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_gre.py b/test/test_gre.py new file mode 100644 index 00000000..9046b05f --- /dev/null +++ b/test/test_gre.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python + +import unittest +from logging import * + +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppDot1QSubint +from vpp_gre_interface import VppGreInterface, VppGre6Interface +from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto, VppIpTable +from vpp_papi_provider import L2_VTR_OP + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q, GRE +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.volatile import RandMAC, RandIP + +from util import ppp, ppc + + +class TestGRE(VppTestCase): + """ GRE Test Case """ + + @classmethod + def setUpClass(cls): + super(TestGRE, cls).setUpClass() + + def setUp(self): + super(TestGRE, self).setUp() + + # create 3 pg interfaces - set one in a non-default table. + self.create_pg_interfaces(range(3)) + + self.tbl = VppIpTable(self, 1) + self.tbl.add_vpp_config() + self.pg1.set_table_ip4(1) + + for i in self.pg_interfaces: + i.admin_up() + + self.pg0.config_ip4() + self.pg0.resolve_arp() + self.pg1.config_ip4() + self.pg1.resolve_arp() + self.pg2.config_ip6() + self.pg2.resolve_ndp() + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.admin_down() + self.pg1.set_table_ip4(0) + super(TestGRE, self).tearDown() + + def create_stream_ip4(self, src_if, src_ip, dst_ip): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_stream_ip6(self, src_if, src_ip, dst_ip): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IPv6(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_4o4(self, src_if, + tunnel_src, tunnel_dst, + src_ip, dst_ip): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=tunnel_src, dst=tunnel_dst) / + GRE() / + IP(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_6o4(self, src_if, + tunnel_src, tunnel_dst, + src_ip, dst_ip): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=tunnel_src, dst=tunnel_dst) / + GRE() / + IPv6(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_6o6(self, src_if, + tunnel_src, tunnel_dst, + src_ip, dst_ip): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IPv6(src=tunnel_src, dst=tunnel_dst) / + GRE() / + IPv6(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_l2o4(self, src_if, + tunnel_src, tunnel_dst): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=tunnel_src, dst=tunnel_dst) / + GRE() / + Ether(dst=RandMAC('*:*:*:*:*:*'), + src=RandMAC('*:*:*:*:*:*')) / + IP(src=str(RandIP()), dst=str(RandIP())) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_tunnel_stream_vlano4(self, src_if, + tunnel_src, tunnel_dst, vlan): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=tunnel_src, dst=tunnel_dst) / + GRE() / + Ether(dst=RandMAC('*:*:*:*:*:*'), + src=RandMAC('*:*:*:*:*:*')) / + Dot1Q(vlan=vlan) / + IP(src=str(RandIP()), dst=str(RandIP())) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def verify_tunneled_4o4(self, src_if, capture, sent, + tunnel_src, tunnel_dst): + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_ip = rx_gre[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_tunneled_6o6(self, src_if, capture, sent, + tunnel_src, tunnel_dst): + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IPv6] + rx_ip = rx[IPv6] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = GRE(str(rx_ip[IPv6].payload)) + rx_ip = rx_gre[IPv6] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_tunneled_l2o4(self, src_if, capture, sent, + tunnel_src, tunnel_dst): + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_l2 = rx_gre[Ether] + rx_ip = rx_l2[IP] + tx_gre = tx[GRE] + tx_l2 = tx_gre[Ether] + tx_ip = tx_l2[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # bridged, not L3 forwarded, so no TTL decrement + self.assertEqual(rx_ip.ttl, tx_ip.ttl) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_tunneled_vlano4(self, src_if, capture, sent, + tunnel_src, tunnel_dst, vlan): + try: + self.assertEqual(len(capture), len(sent)) + except: + ppc("Unexpected packets captured:", capture) + raise + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tunnel_src) + self.assertEqual(rx_ip.dst, tunnel_dst) + + rx_gre = rx[GRE] + rx_l2 = rx_gre[Ether] + rx_vlan = rx_l2[Dot1Q] + rx_ip = rx_l2[IP] + + self.assertEqual(rx_vlan.vlan, vlan) + + tx_gre = tx[GRE] + tx_l2 = tx_gre[Ether] + tx_ip = tx_l2[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # bridged, not L3 forwarded, so no TTL decrement + self.assertEqual(rx_ip.ttl, tx_ip.ttl) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_decapped_4o4(self, src_if, capture, sent): + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + tx_gre = tx[GRE] + tx_ip = tx_gre[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_decapped_6o4(self, src_if, capture, sent): + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IPv6] + tx_gre = tx[GRE] + tx_ip = tx_gre[IPv6] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + self.assertEqual(rx_ip.hlim + 1, tx_ip.hlim) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def test_gre(self): + """ GRE IPv4 tunnel Tests """ + + # + # Create an L3 GRE tunnel. + # - set it admin up + # - assign an IP Addres + # - Add a route via the tunnel + # + gre_if = VppGreInterface(self, + self.pg0.local_ip4, + "1.1.1.2") + gre_if.add_vpp_config() + + # + # The double create (create the same tunnel twice) should fail, + # and we should still be able to use the original + # + try: + gre_if.add_vpp_config() + except Exception: + pass + else: + self.fail("Double GRE tunnel add does not fail") + + gre_if.admin_up() + gre_if.config_ip4() + + route_via_tun = VppIpRoute(self, "4.4.4.4", 32, + [VppRoutePath("0.0.0.0", + gre_if.sw_if_index)]) + + route_via_tun.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - they are all dropped since the tunnel's desintation IP + # is unresolved - or resolves via the default route - which + # which is a drop. + # + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "4.4.4.4") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured( + remark="GRE packets forwarded without DIP resolved") + + # + # Add a route that resolves the tunnel's destination + # + route_tun_dst = VppIpRoute(self, "1.1.1.2", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index)]) + route_tun_dst.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - packets are GRE encapped + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "4.4.4.4") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_tunneled_4o4(self.pg0, rx, tx, + self.pg0.local_ip4, "1.1.1.2") + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_decapped_4o4(self.pg0, rx, tx) + + # + # Send tunneled packets that do not match the tunnel's src + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg0, + "1.1.1.3", + self.pg0.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured( + remark="GRE packets forwarded despite no SRC address match") + + # + # Configure IPv6 on the PG interface so we can route IPv6 + # packets + # + self.pg0.config_ip6() + self.pg0.resolve_ndp() + + # + # Send IPv6 tunnel encapslated packets + # - dropped since IPv6 is not enabled on the tunnel + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_6o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip6, + self.pg0.remote_ip6) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured(remark="IPv6 GRE packets forwarded " + "despite IPv6 not enabled on tunnel") + + # + # Enable IPv6 on the tunnel + # + gre_if.config_ip6() + + # + # Send IPv6 tunnel encapslated packets + # - forwarded since IPv6 is enabled on the tunnel + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_6o4(self.pg0, + "1.1.1.2", + self.pg0.local_ip4, + self.pg0.local_ip6, + self.pg0.remote_ip6) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_decapped_6o4(self.pg0, rx, tx) + + # + # test case cleanup + # + route_tun_dst.remove_vpp_config() + route_via_tun.remove_vpp_config() + gre_if.remove_vpp_config() + + self.pg0.unconfig_ip6() + + def test_gre6(self): + """ GRE IPv6 tunnel Tests """ + + # + # Create an L3 GRE tunnel. + # - set it admin up + # - assign an IP Address + # - Add a route via the tunnel + # + gre_if = VppGre6Interface(self, + self.pg2.local_ip6, + "1002::1") + gre_if.add_vpp_config() + gre_if.admin_up() + gre_if.config_ip6() + + route_via_tun = VppIpRoute( + self, "4004::1", 128, + [VppRoutePath("0::0", + gre_if.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + + route_via_tun.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - they are all dropped since the tunnel's desintation IP + # is unresolved - or resolves via the default route - which + # which is a drop. + # + tx = self.create_stream_ip6(self.pg2, "5005::1", "4004::1") + self.pg2.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg2.assert_nothing_captured( + remark="GRE packets forwarded without DIP resolved") + + # + # Add a route that resolves the tunnel's destination + # + route_tun_dst = VppIpRoute( + self, "1002::1", 128, + [VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_tun_dst.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # - packets are GRE encapped + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg2, "5005::1", "4004::1") + self.pg2.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(len(tx)) + self.verify_tunneled_6o6(self.pg2, rx, tx, + self.pg2.local_ip6, "1002::1") + + # + # test case cleanup + # + route_tun_dst.remove_vpp_config() + route_via_tun.remove_vpp_config() + gre_if.remove_vpp_config() + + self.pg2.unconfig_ip6() + + def test_gre_vrf(self): + """ GRE tunnel VRF Tests """ + + # + # Create an L3 GRE tunnel whose destination is in the non-default + # table. The underlay is thus non-default - the overlay is still + # the default. + # - set it admin up + # - assign an IP Addres + # + gre_if = VppGreInterface(self, self.pg1.local_ip4, + "2.2.2.2", + outer_fib_id=1) + gre_if.add_vpp_config() + gre_if.admin_up() + gre_if.config_ip4() + + # + # Add a route via the tunnel - in the overlay + # + route_via_tun = VppIpRoute(self, "9.9.9.9", 32, + [VppRoutePath("0.0.0.0", + gre_if.sw_if_index)]) + route_via_tun.add_vpp_config() + + # + # Add a route that resolves the tunnel's destination - in the + # underlay table + # + route_tun_dst = VppIpRoute(self, "2.2.2.2", 32, table_id=1, + paths=[VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_tun_dst.add_vpp_config() + + # + # Send a packet stream that is routed into the tunnel + # packets are sent in on pg0 which is in the default table + # - packets are GRE encapped + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "5.5.5.5", "9.9.9.9") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(len(tx)) + self.verify_tunneled_4o4(self.pg1, rx, tx, + self.pg1.local_ip4, "2.2.2.2") + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded. This tests the decap lookup + # does not happen in the encap table + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_4o4(self.pg1, + "2.2.2.2", + self.pg1.local_ip4, + self.pg0.local_ip4, + self.pg0.remote_ip4) + self.pg1.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_decapped_4o4(self.pg0, rx, tx) + + # + # test case cleanup + # + route_tun_dst.remove_vpp_config() + route_via_tun.remove_vpp_config() + gre_if.remove_vpp_config() + + def test_gre_l2(self): + """ GRE tunnel L2 Tests """ + + # + # Add routes to resolve the tunnel destinations + # + route_tun1_dst = VppIpRoute(self, "2.2.2.2", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index)]) + route_tun2_dst = VppIpRoute(self, "2.2.2.3", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index)]) + + route_tun1_dst.add_vpp_config() + route_tun2_dst.add_vpp_config() + + # + # Create 2 L2 GRE tunnels and x-connect them + # + gre_if1 = VppGreInterface(self, self.pg0.local_ip4, + "2.2.2.2", + is_teb=1) + gre_if2 = VppGreInterface(self, self.pg0.local_ip4, + "2.2.2.3", + is_teb=1) + gre_if1.add_vpp_config() + gre_if2.add_vpp_config() + + gre_if1.admin_up() + gre_if2.admin_up() + + self.vapi.sw_interface_set_l2_xconnect(gre_if1.sw_if_index, + gre_if2.sw_if_index, + enable=1) + self.vapi.sw_interface_set_l2_xconnect(gre_if2.sw_if_index, + gre_if1.sw_if_index, + enable=1) + + # + # Send in tunnel encapped L2. expect out tunnel encapped L2 + # in both directions + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_l2o4(self.pg0, + "2.2.2.2", + self.pg0.local_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_tunneled_l2o4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.3") + + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_l2o4(self.pg0, + "2.2.2.3", + self.pg0.local_ip4) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_tunneled_l2o4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.2") + + self.vapi.sw_interface_set_l2_xconnect(gre_if1.sw_if_index, + gre_if2.sw_if_index, + enable=0) + self.vapi.sw_interface_set_l2_xconnect(gre_if2.sw_if_index, + gre_if1.sw_if_index, + enable=0) + + # + # Create a VLAN sub-interfaces on the GRE TEB interfaces + # then x-connect them + # + gre_if_11 = VppDot1QSubint(self, gre_if1, 11) + gre_if_12 = VppDot1QSubint(self, gre_if2, 12) + + # gre_if_11.add_vpp_config() + # gre_if_12.add_vpp_config() + + gre_if_11.admin_up() + gre_if_12.admin_up() + + self.vapi.sw_interface_set_l2_xconnect(gre_if_11.sw_if_index, + gre_if_12.sw_if_index, + enable=1) + self.vapi.sw_interface_set_l2_xconnect(gre_if_12.sw_if_index, + gre_if_11.sw_if_index, + enable=1) + + # + # Configure both to pop thier respective VLAN tags, + # so that during the x-coonect they will subsequently push + # + self.vapi.sw_interface_set_l2_tag_rewrite(gre_if_12.sw_if_index, + L2_VTR_OP.L2_POP_1, + 12) + self.vapi.sw_interface_set_l2_tag_rewrite(gre_if_11.sw_if_index, + L2_VTR_OP.L2_POP_1, + 11) + + # + # Send traffic in both directiond - expect the VLAN tags to + # be swapped. + # + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_vlano4(self.pg0, + "2.2.2.2", + self.pg0.local_ip4, + 11) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_tunneled_vlano4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.3", + 12) + + self.vapi.cli("clear trace") + tx = self.create_tunnel_stream_vlano4(self.pg0, + "2.2.2.3", + self.pg0.local_ip4, + 12) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(len(tx)) + self.verify_tunneled_vlano4(self.pg0, rx, tx, + self.pg0.local_ip4, + "2.2.2.2", + 11) + + # + # Cleanup Test resources + # + gre_if_11.remove_vpp_config() + gre_if_12.remove_vpp_config() + gre_if1.remove_vpp_config() + gre_if2.remove_vpp_config() + route_tun1_dst.add_vpp_config() + route_tun2_dst.add_vpp_config() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_gtpu.py b/test/test_gtpu.py new file mode 100644 index 00000000..9411fffc --- /dev/null +++ b/test/test_gtpu.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python + +import socket +from util import ip4n_range +import unittest +from framework import VppTestCase, VppTestRunner +from template_bd import BridgeDomain + +from scapy.layers.l2 import Ether, Raw +from scapy.layers.inet import IP, UDP +from scapy.contrib.gtp import GTP_U_Header +from scapy.utils import atol + + +class TestGtpu(BridgeDomain, VppTestCase): + """ GTPU Test Case """ + + def __init__(self, *args): + BridgeDomain.__init__(self) + VppTestCase.__init__(self, *args) + + def encapsulate(self, pkt, vni): + """ + Encapsulate the original payload frame by adding GTPU header with its + UDP, IP and Ethernet fields + """ + return (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + GTP_U_Header(TEID=vni, gtp_type=self.gtp_type, length=150) / + pkt) + + def encap_mcast(self, pkt, src_ip, src_mac, vni): + """ + Encapsulate the original payload frame by adding GTPU header with its + UDP, IP and Ethernet fields + """ + return (Ether(src=src_mac, dst=self.mcast_mac) / + IP(src=src_ip, dst=self.mcast_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + GTP_U_Header(TEID=vni, gtp_type=self.gtp_type, length=150) / + pkt) + + def decapsulate(self, pkt): + """ + Decapsulate the original payload frame by removing GTPU header + """ + return pkt[GTP_U_Header].payload + + # Method for checking GTPU encapsulation. + # + def check_encapsulation(self, pkt, vni, local_only=False, mcast_pkt=False): + # Verify source MAC is VPP_MAC and destination MAC is MY_MAC resolved + # by VPP using ARP. + self.assertEqual(pkt[Ether].src, self.pg0.local_mac) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[Ether].dst, self.pg0.remote_mac) + else: + self.assertEqual(pkt[Ether].dst, type(self).mcast_mac) + # Verify GTPU tunnel source IP is VPP_IP and destination IP is MY_IP. + self.assertEqual(pkt[IP].src, self.pg0.local_ip4) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[IP].dst, self.pg0.remote_ip4) + else: + self.assertEqual(pkt[IP].dst, type(self).mcast_ip4) + # Verify UDP destination port is GTPU 2152, source UDP port could be + # arbitrary. + self.assertEqual(pkt[UDP].dport, type(self).dport) + # Verify TEID + self.assertEqual(pkt[GTP_U_Header].TEID, vni) + + def test_encap(self): + """ Encapsulation test + Send frames from pg1 + Verify receipt of encapsulated frames on pg0 + """ + self.pg1.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.single_tunnel_bd) + + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + def test_ucast_flood(self): + """ Unicast flood test + Send frames from pg3 + Verify receipt of encapsulated frames on pg0 + """ + self.pg3.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Get packet from each tunnel and assert it's corectly encapsulated. + out = self.pg0.get_capture(self.n_ucast_tunnels) + for pkt in out: + self.check_encapsulation(pkt, self.ucast_flood_bd, True) + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + def test_mcast_flood(self): + """ Multicast flood test + Send frames from pg2 + Verify receipt of encapsulated frames on pg0 + """ + self.pg2.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.mcast_flood_bd, + local_only=False, mcast_pkt=True) + + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + @classmethod + def create_gtpu_flood_test_bd(cls, teid, n_ucast_tunnels): + # Create 10 ucast gtpu tunnels under bd + ip_range_start = 10 + ip_range_end = ip_range_start + n_ucast_tunnels + next_hop_address = cls.pg0.remote_ip4n + for dest_ip4n in ip4n_range(next_hop_address, ip_range_start, + ip_range_end): + # add host route so dest_ip4n will not be resolved + cls.vapi.ip_add_del_route(dest_ip4n, 32, next_hop_address) + r = cls.vapi.gtpu_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + teid=teid) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, bd_id=teid) + + @classmethod + def add_del_shared_mcast_dst_load(cls, is_add): + """ + add or del tunnels sharing the same mcast dst + to test gtpu ref_count mechanism + """ + n_shared_dst_tunnels = 20 + teid_start = 1000 + teid_end = teid_start + n_shared_dst_tunnels + for teid in range(teid_start, teid_end): + r = cls.vapi.gtpu_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + teid=teid, + is_add=is_add) + if r.sw_if_index == 0xffffffff: + raise "bad sw_if_index" + + @classmethod + def add_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=1) + + @classmethod + def del_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=0) + + @classmethod + def add_del_mcast_tunnels_load(cls, is_add): + """ + add or del tunnels to test gtpu stability + """ + n_distinct_dst_tunnels = 20 + ip_range_start = 10 + ip_range_end = ip_range_start + n_distinct_dst_tunnels + for dest_ip4n in ip4n_range(cls.mcast_ip4n, ip_range_start, + ip_range_end): + teid = bytearray(dest_ip4n)[3] + cls.vapi.gtpu_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + mcast_sw_if_index=1, + teid=teid, + is_add=is_add) + + @classmethod + def add_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=1) + + @classmethod + def del_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=0) + + # Class method to start the GTPU test case. + # Overrides setUpClass method in VppTestCase class. + # Python try..except statement is used to ensure that the tear down of + # the class will be executed even if exception is raised. + # @param cls The class pointer. + @classmethod + def setUpClass(cls): + super(TestGtpu, cls).setUpClass() + + try: + cls.dport = 2152 + cls.gtp_type = 0xff + + # Create 2 pg interfaces. + cls.create_pg_interfaces(range(4)) + for pg in cls.pg_interfaces: + pg.admin_up() + + # Configure IPv4 addresses on VPP pg0. + cls.pg0.config_ip4() + + # Resolve MAC address for VPP's IP address on pg0. + cls.pg0.resolve_arp() + + # Our Multicast address + cls.mcast_ip4 = '239.1.1.1' + cls.mcast_ip4n = socket.inet_pton(socket.AF_INET, cls.mcast_ip4) + iplong = atol(cls.mcast_ip4) + cls.mcast_mac = "01:00:5e:%02x:%02x:%02x" % ( + (iplong >> 16) & 0x7F, (iplong >> 8) & 0xFF, iplong & 0xFF) + + # Create GTPU VTEP on VPP pg0, and put gtpu_tunnel0 and pg1 + # into BD. + cls.single_tunnel_bd = 11 + r = cls.vapi.gtpu_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.pg0.remote_ip4n, + teid=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg1.sw_if_index, + bd_id=cls.single_tunnel_bd) + + # Setup teid 2 to test multicast flooding + cls.n_ucast_tunnels = 10 + cls.mcast_flood_bd = 12 + cls.create_gtpu_flood_test_bd(cls.mcast_flood_bd, + cls.n_ucast_tunnels) + r = cls.vapi.gtpu_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + teid=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg2.sw_if_index, + bd_id=cls.mcast_flood_bd) + + # Add and delete mcast tunnels to check stability + cls.add_shared_mcast_dst_load() + cls.add_mcast_tunnels_load() + cls.del_shared_mcast_dst_load() + cls.del_mcast_tunnels_load() + + # Setup teid 3 to test unicast flooding + cls.ucast_flood_bd = 13 + cls.create_gtpu_flood_test_bd(cls.ucast_flood_bd, + cls.n_ucast_tunnels) + cls.vapi.sw_interface_set_l2_bridge(cls.pg3.sw_if_index, + bd_id=cls.ucast_flood_bd) + except Exception: + super(TestGtpu, cls).tearDownClass() + raise + + # Method to define VPP actions before tear down of the test case. + # Overrides tearDown method in VppTestCase class. + # @param self The object pointer. + def tearDown(self): + super(TestGtpu, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show bridge-domain 11 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 12 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 13 detail")) + self.logger.info(self.vapi.cli("show int")) + self.logger.info(self.vapi.cli("show gtpu tunnel")) + self.logger.info(self.vapi.cli("show trace")) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_interface_crud.py b/test/test_interface_crud.py new file mode 100644 index 00000000..63917047 --- /dev/null +++ b/test/test_interface_crud.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +"""CRUD tests of APIs (Create, Read, Update, Delete) HLD: + +- interface up/down/add/delete - interface type: + - pg (TBD) + - loopback + - vhostuser (TBD) + - af_packet (TBD) + - netmap (TBD) + - tuntap (root privileges needed) + - vxlan (TBD) +""" + +import unittest + +from scapy.layers.inet import IP, ICMP +from scapy.layers.l2 import Ether + +from framework import VppTestCase, VppTestRunner + + +class TestLoopbackInterfaceCRUD(VppTestCase): + """CRUD Loopback + + """ + + @classmethod + def setUpClass(cls): + super(TestLoopbackInterfaceCRUD, cls).setUpClass() + try: + cls.create_pg_interfaces(range(1)) + for i in cls.pg_interfaces: + i.config_ip4() + i.resolve_arp() + except: + cls.tearDownClass() + raise + + @staticmethod + def create_icmp_stream(src_if, dst_ifs): + """ + + :param VppInterface src_if: Packets are send to this interface, + using this interfaces remote host. + :param list dst_ifs: IPv4 ICMP requests are send to interfaces + addresses. + :return: List of generated packets. + """ + pkts = [] + for i in dst_ifs: + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=i.local_ip4) / + ICMP(id=i.sw_if_index, type='echo-request')) + pkts.append(p) + return pkts + + def verify_icmp(self, capture, request_src_if, dst_ifs): + """ + + :param capture: Capture to verify. + :param VppInterface request_src_if: Interface where was send packets. + :param list dst_ifs: Interfaces where was generated IPv4 ICMP requests. + """ + rcvd_icmp_pkts = [] + for pkt in capture: + try: + ip = pkt[IP] + icmp = pkt[ICMP] + except IndexError: + pass + else: + info = (ip.src, ip.dst, icmp.type, icmp.id) + rcvd_icmp_pkts.append(info) + + for i in dst_ifs: + # 0 - icmp echo response + info = (i.local_ip4, request_src_if.remote_ip4, 0, i.sw_if_index) + self.assertIn(info, rcvd_icmp_pkts) + + def test_crud(self): + # create + loopbacks = self.create_loopback_interfaces(range(20)) + for i in loopbacks: + i.local_ip4_prefix_len = 32 + i.config_ip4() + i.admin_up() + + # read (check sw if dump, ip4 fib, ip6 fib) + if_dump = self.vapi.sw_interface_dump() + fib4_dump = self.vapi.ip_fib_dump() + for i in loopbacks: + self.assertTrue(i.is_interface_config_in_dump(if_dump)) + self.assertTrue(i.is_ip4_entry_in_fib_dump(fib4_dump)) + + # check ping + stream = self.create_icmp_stream(self.pg0, loopbacks) + self.pg0.add_stream(stream) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(expected_count=len(stream)) + + self.verify_icmp(capture, self.pg0, loopbacks) + + # delete + for i in loopbacks: + i.remove_vpp_config() + + # read (check not in sw if dump, ip4 fib, ip6 fib) + if_dump = self.vapi.sw_interface_dump() + fib4_dump = self.vapi.ip_fib_dump() + for i in loopbacks: + self.assertFalse(i.is_interface_config_in_dump(if_dump)) + self.assertFalse(i.is_ip4_entry_in_fib_dump(fib4_dump)) + + # check not ping + stream = self.create_icmp_stream(self.pg0, loopbacks) + self.pg0.add_stream(stream) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.assert_nothing_captured() + + def test_down(self): + # create + loopbacks = self.create_loopback_interfaces(range(20)) + for i in loopbacks: + i.local_ip4_prefix_len = 32 + i.config_ip4() + i.admin_up() + + # disable + for i in loopbacks: + i.admin_down() + i.unconfig_ip4() + + # read (check not in sw if dump, ip4 fib, ip6 fib) + if_dump = self.vapi.sw_interface_dump() + fib4_dump = self.vapi.ip_fib_dump() + for i in loopbacks: + self.assertTrue(i.is_interface_config_in_dump(if_dump)) + self.assertFalse(i.is_ip4_entry_in_fib_dump(fib4_dump)) + + # check not ping + stream = self.create_icmp_stream(self.pg0, loopbacks) + self.pg0.add_stream(stream) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.assert_nothing_captured() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip4.py b/test/test_ip4.py new file mode 100644 index 00000000..55d16735 --- /dev/null +++ b/test/test_ip4.py @@ -0,0 +1,1013 @@ +#!/usr/bin/env python +import random +import socket +import unittest + +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint +from vpp_ip_route import VppIpRoute, VppRoutePath, VppIpMRoute, \ + VppMRoutePath, MRouteItfFlags, MRouteEntryFlags, VppMplsIpBind, \ + VppMplsTable + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q, ARP +from scapy.layers.inet import IP, UDP, ICMP, icmptypes, icmpcodes +from util import ppp +from scapy.contrib.mpls import MPLS + + +class TestIPv4(VppTestCase): + """ IPv4 Test Case """ + + def setUp(self): + """ + Perform test setup before test case. + + **Config:** + - create 3 pg interfaces + - untagged pg0 interface + - Dot1Q subinterface on pg1 + - Dot1AD subinterface on pg2 + - setup interfaces: + - put it into UP state + - set IPv4 addresses + - resolve neighbor address using ARP + - configure 200 fib entries + + :ivar list interfaces: pg interfaces and subinterfaces. + :ivar dict flows: IPv4 packet flows in test. + :ivar list pg_if_packet_sizes: packet sizes in test. + """ + super(TestIPv4, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) + + # create 2 subinterfaces for pg1 and pg2 + self.sub_interfaces = [ + VppDot1QSubint(self, self.pg1, 100), + VppDot1ADSubint(self, self.pg2, 200, 300, 400)] + + # packet flows mapping pg0 -> pg1.sub, pg2.sub, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1.sub_if, self.pg2.sub_if] + self.flows[self.pg1.sub_if] = [self.pg0, self.pg2.sub_if] + self.flows[self.pg2.sub_if] = [self.pg0, self.pg1.sub_if] + + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] + self.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] + + self.interfaces = list(self.pg_interfaces) + self.interfaces.extend(self.sub_interfaces) + + # setup all interfaces + for i in self.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + # config 2M FIB entries + self.config_fib_entries(200) + + def tearDown(self): + """Run standard test teardown and log ``show ip arp``.""" + super(TestIPv4, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show ip arp")) + # info(self.vapi.cli("show ip fib")) # many entries + + def config_fib_entries(self, count): + """For each interface add to the FIB table *count* routes to + "10.0.0.1/32" destination with interface's local address as next-hop + address. + + :param int count: Number of FIB entries. + + - *TODO:* check if the next-hop address shouldn't be remote address + instead of local address. + """ + n_int = len(self.interfaces) + percent = 0 + counter = 0.0 + dest_addr = socket.inet_pton(socket.AF_INET, "10.0.0.1") + dest_addr_len = 32 + for i in self.interfaces: + next_hop_address = i.local_ip4n + for j in range(count / n_int): + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, next_hop_address) + counter += 1 + if counter / count * 100 > percent: + self.logger.info("Configure %d FIB entries .. %d%% done" % + (count, percent)) + percent += 1 + + def create_stream(self, src_if, packet_sizes): + """Create input packet stream for defined interface. + + :param VppInterface src_if: Interface to create packet stream for. + :param list packet_sizes: Required packet sizes. + """ + pkts = [] + for i in range(0, 257): + dst_if = self.flows[src_if][i % 2] + info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + if isinstance(src_if, VppSubInterface): + p = src_if.add_dot1_layer(p) + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, dst_if, capture): + """Verify captured input packet stream for defined interface. + + :param VppInterface dst_if: Interface to verify captured packet stream + for. + :param list capture: Captured packet stream. + """ + self.logger.info("Verifying capture on interface %s" % dst_if.name) + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + is_sub_if = False + dst_sw_if_index = dst_if.sw_if_index + if hasattr(dst_if, 'parent'): + is_sub_if = True + for packet in capture: + if is_sub_if: + # Check VLAN tags and Ethernet header + packet = dst_if.remove_dot1_layer(packet) + self.assertTrue(Dot1Q not in packet) + try: + ip = packet[IP] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug( + "Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) + + def test_fib(self): + """ IPv4 FIB test + + Test scenario: + + - Create IPv4 stream for pg0 interface + - Create IPv4 tagged streams for pg1's and pg2's subinterface. + - Send and verify received packets on each interface. + """ + + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + for i in self.sub_interfaces: + pkts = self.create_stream(i, self.sub_if_packet_sizes) + i.parent.add_stream(pkts) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg0.get_capture() + self.verify_capture(self.pg0, pkts) + + for i in self.sub_interfaces: + pkts = i.parent.get_capture() + self.verify_capture(i, pkts) + + +class TestIPv4FibCrud(VppTestCase): + """ FIB - add/update/delete - ip4 routes + + Test scenario: + - add 1k, + - del 100, + - add new 1k, + - del 1.5k + + ..note:: Python API is too slow to add many routes, needs replacement. + """ + + def config_fib_many_to_one(self, start_dest_addr, next_hop_addr, count): + """ + + :param start_dest_addr: + :param next_hop_addr: + :param count: + :return list: added ips with 32 prefix + """ + added_ips = [] + dest_addr = int(socket.inet_pton(socket.AF_INET, + start_dest_addr).encode('hex'), + 16) + dest_addr_len = 32 + n_next_hop_addr = socket.inet_pton(socket.AF_INET, next_hop_addr) + for _ in range(count): + n_dest_addr = '{:08x}'.format(dest_addr).decode('hex') + self.vapi.ip_add_del_route(n_dest_addr, dest_addr_len, + n_next_hop_addr) + added_ips.append(socket.inet_ntoa(n_dest_addr)) + dest_addr += 1 + return added_ips + + def unconfig_fib_many_to_one(self, start_dest_addr, next_hop_addr, count): + + removed_ips = [] + dest_addr = int(socket.inet_pton(socket.AF_INET, + start_dest_addr).encode('hex'), + 16) + dest_addr_len = 32 + n_next_hop_addr = socket.inet_pton(socket.AF_INET, next_hop_addr) + for _ in range(count): + n_dest_addr = '{:08x}'.format(dest_addr).decode('hex') + self.vapi.ip_add_del_route(n_dest_addr, dest_addr_len, + n_next_hop_addr, is_add=0) + removed_ips.append(socket.inet_ntoa(n_dest_addr)) + dest_addr += 1 + return removed_ips + + def create_stream(self, src_if, dst_if, dst_ips, count): + pkts = [] + + for _ in range(count): + dst_addr = random.choice(dst_ips) + info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_addr) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + self.extend_packet(p, random.choice(self.pg_if_packet_sizes)) + pkts.append(p) + + return pkts + + def _find_ip_match(self, find_in, pkt): + for p in find_in: + if self.payload_to_info(str(p[Raw])) == \ + self.payload_to_info(str(pkt[Raw])): + if p[IP].src != pkt[IP].src: + break + if p[IP].dst != pkt[IP].dst: + break + if p[UDP].sport != pkt[UDP].sport: + break + if p[UDP].dport != pkt[UDP].dport: + break + return p + return None + + @staticmethod + def _match_route_detail(route_detail, ip, address_length=32, table_id=0): + if route_detail.address == socket.inet_pton(socket.AF_INET, ip): + if route_detail.table_id != table_id: + return False + elif route_detail.address_length != address_length: + return False + else: + return True + else: + return False + + def verify_capture(self, dst_interface, received_pkts, expected_pkts): + self.assertEqual(len(received_pkts), len(expected_pkts)) + to_verify = list(expected_pkts) + for p in received_pkts: + self.assertEqual(p.src, dst_interface.local_mac) + self.assertEqual(p.dst, dst_interface.remote_mac) + x = self._find_ip_match(to_verify, p) + to_verify.remove(x) + self.assertListEqual(to_verify, []) + + def verify_route_dump(self, fib_dump, ips): + + def _ip_in_route_dump(ip, fib_dump): + return next((route for route in fib_dump + if self._match_route_detail(route, ip)), + False) + + for ip in ips: + self.assertTrue(_ip_in_route_dump(ip, fib_dump), + 'IP {} is not in fib dump.'.format(ip)) + + def verify_not_in_route_dump(self, fib_dump, ips): + + def _ip_in_route_dump(ip, fib_dump): + return next((route for route in fib_dump + if self._match_route_detail(route, ip)), + False) + + for ip in ips: + self.assertFalse(_ip_in_route_dump(ip, fib_dump), + 'IP {} is in fib dump.'.format(ip)) + + @classmethod + def setUpClass(cls): + """ + #. Create and initialize 3 pg interfaces. + #. initialize class attributes configured_routes and deleted_routes + to store information between tests. + """ + super(TestIPv4FibCrud, cls).setUpClass() + + try: + # create 3 pg interfaces + cls.create_pg_interfaces(range(3)) + + cls.interfaces = list(cls.pg_interfaces) + + # setup all interfaces + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + cls.configured_routes = [] + cls.deleted_routes = [] + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + except Exception: + super(TestIPv4FibCrud, cls).tearDownClass() + raise + + def setUp(self): + super(TestIPv4FibCrud, self).setUp() + self.reset_packet_infos() + + def test_1_add_routes(self): + """ Add 1k routes + + - add 100 routes check with traffic script. + """ + # config 1M FIB entries + self.configured_routes.extend(self.config_fib_many_to_one( + "10.0.0.0", self.pg0.remote_ip4, 100)) + + fib_dump = self.vapi.ip_fib_dump() + self.verify_route_dump(fib_dump, self.configured_routes) + + self.stream_1 = self.create_stream( + self.pg1, self.pg0, self.configured_routes, 100) + self.stream_2 = self.create_stream( + self.pg2, self.pg0, self.configured_routes, 100) + self.pg1.add_stream(self.stream_1) + self.pg2.add_stream(self.stream_2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) + self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) + + def test_2_del_routes(self): + """ Delete 100 routes + + - delete 10 routes check with traffic script. + """ + self.deleted_routes.extend(self.unconfig_fib_many_to_one( + "10.0.0.10", self.pg0.remote_ip4, 10)) + for x in self.deleted_routes: + self.configured_routes.remove(x) + + fib_dump = self.vapi.ip_fib_dump() + self.verify_route_dump(fib_dump, self.configured_routes) + + self.stream_1 = self.create_stream( + self.pg1, self.pg0, self.configured_routes, 100) + self.stream_2 = self.create_stream( + self.pg2, self.pg0, self.configured_routes, 100) + self.stream_3 = self.create_stream( + self.pg1, self.pg0, self.deleted_routes, 100) + self.stream_4 = self.create_stream( + self.pg2, self.pg0, self.deleted_routes, 100) + self.pg1.add_stream(self.stream_1 + self.stream_3) + self.pg2.add_stream(self.stream_2 + self.stream_4) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) + self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) + + def test_3_add_new_routes(self): + """ Add 1k routes + + - re-add 5 routes check with traffic script. + - add 100 routes check with traffic script. + """ + tmp = self.config_fib_many_to_one( + "10.0.0.10", self.pg0.remote_ip4, 5) + self.configured_routes.extend(tmp) + for x in tmp: + self.deleted_routes.remove(x) + + self.configured_routes.extend(self.config_fib_many_to_one( + "10.0.1.0", self.pg0.remote_ip4, 100)) + + fib_dump = self.vapi.ip_fib_dump() + self.verify_route_dump(fib_dump, self.configured_routes) + + self.stream_1 = self.create_stream( + self.pg1, self.pg0, self.configured_routes, 300) + self.stream_2 = self.create_stream( + self.pg2, self.pg0, self.configured_routes, 300) + self.stream_3 = self.create_stream( + self.pg1, self.pg0, self.deleted_routes, 100) + self.stream_4 = self.create_stream( + self.pg2, self.pg0, self.deleted_routes, 100) + + self.pg1.add_stream(self.stream_1 + self.stream_3) + self.pg2.add_stream(self.stream_2 + self.stream_4) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg0.get_capture(len(self.stream_1) + len(self.stream_2)) + self.verify_capture(self.pg0, pkts, self.stream_1 + self.stream_2) + + def test_4_del_routes(self): + """ Delete 1.5k routes + + - delete 5 routes check with traffic script. + - add 100 routes check with traffic script. + """ + self.deleted_routes.extend(self.unconfig_fib_many_to_one( + "10.0.0.0", self.pg0.remote_ip4, 15)) + self.deleted_routes.extend(self.unconfig_fib_many_to_one( + "10.0.0.20", self.pg0.remote_ip4, 85)) + self.deleted_routes.extend(self.unconfig_fib_many_to_one( + "10.0.1.0", self.pg0.remote_ip4, 100)) + fib_dump = self.vapi.ip_fib_dump() + self.verify_not_in_route_dump(fib_dump, self.deleted_routes) + + +class TestIPNull(VppTestCase): + """ IPv4 routes via NULL """ + + def setUp(self): + super(TestIPNull, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(1)) + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + def tearDown(self): + super(TestIPNull, self).tearDown() + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + def test_ip_null(self): + """ IP NULL route """ + + # + # A route via IP NULL that will reply with ICMP unreachables + # + ip_unreach = VppIpRoute(self, "10.0.0.1", 32, [], is_unreach=1) + ip_unreach.add_vpp_config() + + p_unreach = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst="10.0.0.1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(p_unreach) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + rx = rx[0] + icmp = rx[ICMP] + + self.assertEqual(icmptypes[icmp.type], "dest-unreach") + self.assertEqual(icmpcodes[icmp.type][icmp.code], "host-unreachable") + self.assertEqual(icmp.src, self.pg0.remote_ip4) + self.assertEqual(icmp.dst, "10.0.0.1") + + # + # ICMP replies are rate limited. so sit and spin. + # + self.sleep(1) + + # + # A route via IP NULL that will reply with ICMP prohibited + # + ip_prohibit = VppIpRoute(self, "10.0.0.2", 32, [], is_prohibit=1) + ip_prohibit.add_vpp_config() + + p_prohibit = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst="10.0.0.2") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(p_prohibit) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + + rx = rx[0] + icmp = rx[ICMP] + + self.assertEqual(icmptypes[icmp.type], "dest-unreach") + self.assertEqual(icmpcodes[icmp.type][icmp.code], "host-prohibited") + self.assertEqual(icmp.src, self.pg0.remote_ip4) + self.assertEqual(icmp.dst, "10.0.0.2") + + +class TestIPDisabled(VppTestCase): + """ IPv4 disabled """ + + def setUp(self): + super(TestIPDisabled, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(2)) + + # PG0 is IP enalbed + self.pg0.admin_up() + self.pg0.config_ip4() + self.pg0.resolve_arp() + + # PG 1 is not IP enabled + self.pg1.admin_up() + + def tearDown(self): + super(TestIPDisabled, self).tearDown() + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) + + def test_ip_disabled(self): + """ IP Disabled """ + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_232_1_1_1.add_vpp_config() + + pu = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(src="10.10.10.10", dst=self.pg0.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + pm = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(src="10.10.10.10", dst="232.1.1.1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, pu, "IP disabled") + self.send_and_assert_no_replies(self.pg1, pm, "IP disabled") + + # + # IP enable PG1 + # + self.pg1.config_ip4() + + # + # Now we get packets through + # + self.pg1.add_stream(pu) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + + self.pg1.add_stream(pm) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + + # + # Disable PG1 + # + self.pg1.unconfig_ip4() + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, pu, "IP disabled") + self.send_and_assert_no_replies(self.pg1, pm, "IP disabled") + + +class TestIPSubNets(VppTestCase): + """ IPv4 Subnets """ + + def setUp(self): + super(TestIPSubNets, self).setUp() + + # create a 2 pg interfaces + self.create_pg_interfaces(range(2)) + + # pg0 we will use to experiemnt + self.pg0.admin_up() + + # pg1 is setup normally + self.pg1.admin_up() + self.pg1.config_ip4() + self.pg1.resolve_arp() + + def tearDown(self): + super(TestIPSubNets, self).tearDown() + for i in self.pg_interfaces: + i.admin_down() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) + + def test_ip_sub_nets(self): + """ IP Sub Nets """ + + # + # Configure a covering route to forward so we know + # when we are dropping + # + cover_route = VppIpRoute(self, "10.0.0.0", 8, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + cover_route.add_vpp_config() + + p = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(dst="10.10.10.10", src=self.pg0.local_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + + # + # Configure some non-/24 subnets on an IP interface + # + ip_addr_n = socket.inet_pton(socket.AF_INET, "10.10.10.10") + + self.vapi.sw_interface_add_del_address(self.pg0.sw_if_index, + ip_addr_n, + 16) + + pn = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(dst="10.10.0.0", src=self.pg0.local_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + pb = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(dst="10.10.255.255", src=self.pg0.local_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.send_and_assert_no_replies(self.pg1, pn, "IP Network address") + self.send_and_assert_no_replies(self.pg1, pb, "IP Broadcast address") + + # remove the sub-net and we are forwarding via the cover again + self.vapi.sw_interface_add_del_address(self.pg0.sw_if_index, + ip_addr_n, + 16, + is_add=0) + self.pg1.add_stream(pn) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + self.pg1.add_stream(pb) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + + # + # A /31 is a special case where the 'other-side' is an attached host + # packets to that peer generate ARP requests + # + ip_addr_n = socket.inet_pton(socket.AF_INET, "10.10.10.10") + + self.vapi.sw_interface_add_del_address(self.pg0.sw_if_index, + ip_addr_n, + 31) + + pn = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IP(dst="10.10.10.11", src=self.pg0.local_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg1.add_stream(pn) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + rx[ARP] + + # remove the sub-net and we are forwarding via the cover again + self.vapi.sw_interface_add_del_address(self.pg0.sw_if_index, + ip_addr_n, + 31, + is_add=0) + self.pg1.add_stream(pn) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + + +class TestIPLoadBalance(VppTestCase): + """ IPv4 Load-Balancing """ + + def setUp(self): + super(TestIPLoadBalance, self).setUp() + + self.create_pg_interfaces(range(5)) + mpls_tbl = VppMplsTable(self, 0) + mpls_tbl.add_vpp_config() + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + i.enable_mpls() + + def tearDown(self): + for i in self.pg_interfaces: + i.disable_mpls() + i.unconfig_ip4() + i.admin_down() + super(TestIPLoadBalance, self).tearDown() + + def send_and_expect_load_balancing(self, input, pkts, outputs): + input.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for oo in outputs: + rx = oo._get_capture(1) + self.assertNotEqual(0, len(rx)) + + def send_and_expect_one_itf(self, input, pkts, itf): + input.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = itf.get_capture(len(pkts)) + + def test_ip_load_balance(self): + """ IP Load-Balancing """ + + # + # An array of packets that differ only in the destination port + # + port_ip_pkts = [] + port_mpls_pkts = [] + + # + # An array of packets that differ only in the source address + # + src_ip_pkts = [] + src_mpls_pkts = [] + + for ii in range(65): + port_ip_hdr = (IP(dst="10.0.0.1", src="20.0.0.1") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100)) + port_ip_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + port_ip_hdr)) + port_mpls_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=66, ttl=2) / + port_ip_hdr)) + + src_ip_hdr = (IP(dst="10.0.0.1", src="20.0.0.%d" % ii) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + src_ip_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + src_ip_hdr)) + src_mpls_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=66, ttl=2) / + src_ip_hdr)) + + route_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index), + VppRoutePath(self.pg2.remote_ip4, + self.pg2.sw_if_index)]) + route_10_0_0_1.add_vpp_config() + + binding = VppMplsIpBind(self, 66, "10.0.0.1", 32) + binding.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across the 2 paths + # - since the default hash config is to use IP src,dst and port + # src,dst + # We are not going to ensure equal amounts of packets across each link, + # since the hash algorithm is statistical and therefore this can never + # be guaranteed. But wuth 64 different packets we do expect some + # balancing. So instead just ensure there is traffic on each link. + # + self.send_and_expect_load_balancing(self.pg0, port_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, port_mpls_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_mpls_pkts, + [self.pg1, self.pg2]) + + # + # change the flow hash config so it's only IP src,dst + # - now only the stream with differing source address will + # load-balance + # + self.vapi.set_ip_flow_hash(0, src=1, dst=1, sport=0, dport=0) + + self.send_and_expect_load_balancing(self.pg0, src_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_mpls_pkts, + [self.pg1, self.pg2]) + + self.send_and_expect_one_itf(self.pg0, port_ip_pkts, self.pg2) + + # + # change the flow hash config back to defaults + # + self.vapi.set_ip_flow_hash(0, src=1, dst=1, sport=1, dport=1) + + # + # Recursive prefixes + # - testing that 2 stages of load-balancing occurs and there is no + # polarisation (i.e. only 2 of 4 paths are used) + # + port_pkts = [] + src_pkts = [] + + for ii in range(257): + port_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(dst="1.1.1.1", src="20.0.0.1") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100))) + src_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(dst="1.1.1.1", src="20.0.0.%d" % ii) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100))) + + route_10_0_0_2 = VppIpRoute(self, "10.0.0.2", 32, + [VppRoutePath(self.pg3.remote_ip4, + self.pg3.sw_if_index), + VppRoutePath(self.pg4.remote_ip4, + self.pg4.sw_if_index)]) + route_10_0_0_2.add_vpp_config() + + route_1_1_1_1 = VppIpRoute(self, "1.1.1.1", 32, + [VppRoutePath("10.0.0.2", 0xffffffff), + VppRoutePath("10.0.0.1", 0xffffffff)]) + route_1_1_1_1.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across all 4 paths + # + self.vapi.cli("clear trace") + self.send_and_expect_load_balancing(self.pg0, port_pkts, + [self.pg1, self.pg2, + self.pg3, self.pg4]) + self.send_and_expect_load_balancing(self.pg0, src_pkts, + [self.pg1, self.pg2, + self.pg3, self.pg4]) + + # + # Recursive prefixes + # - testing that 2 stages of load-balancing, no choices + # + port_pkts = [] + + for ii in range(257): + port_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(dst="1.1.1.2", src="20.0.0.2") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100))) + + route_10_0_0_3 = VppIpRoute(self, "10.0.0.3", 32, + [VppRoutePath(self.pg3.remote_ip4, + self.pg3.sw_if_index)]) + route_10_0_0_3.add_vpp_config() + + route_1_1_1_2 = VppIpRoute(self, "1.1.1.2", 32, + [VppRoutePath("10.0.0.3", 0xffffffff)]) + route_1_1_1_2.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across all 4 paths + # + self.vapi.cli("clear trace") + self.send_and_expect_one_itf(self.pg0, port_pkts, self.pg3) + + +class TestIPVlan0(VppTestCase): + """ IPv4 VLAN-0 """ + + def setUp(self): + super(TestIPVlan0, self).setUp() + + self.create_pg_interfaces(range(2)) + mpls_tbl = VppMplsTable(self, 0) + mpls_tbl.add_vpp_config() + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + i.enable_mpls() + + def tearDown(self): + for i in self.pg_interfaces: + i.disable_mpls() + i.unconfig_ip4() + i.admin_down() + super(TestIPVlan0, self).tearDown() + + def send_and_expect(self, input, pkts, output): + input.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = output.get_capture(len(pkts)) + + def test_ip_vlan_0(self): + """ IP VLAN-0 """ + + pkts = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + Dot1Q(vlan=0) / + IP(dst=self.pg1.remote_ip4, + src=self.pg0.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) * 65 + + # + # Expect that packets sent on VLAN-0 are forwarded on the + # main interface. + # + self.send_and_expect(self.pg0, pkts, self.pg1) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip4_irb.py b/test/test_ip4_irb.py new file mode 100644 index 00000000..bbec7ca7 --- /dev/null +++ b/test/test_ip4_irb.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +"""IRB Test Case HLD: + +**config** + - L2 MAC learning enabled in l2bd + - 2 routed interfaces untagged, bvi (Bridge Virtual Interface) + - 2 bridged interfaces in l2bd with bvi + +**test** + - sending ip4 eth pkts between routed interfaces + - 2 routed interfaces + - 2 bridged interfaces + + - 64B, 512B, 1518B, 9200B (ether_size) + + - burst of pkts per interface + - 257pkts per burst + - routed pkts hitting different FIB entries + - bridged pkts hitting different MAC entries + +**verify** + - all packets received correctly + +""" + +import unittest +from random import choice + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner + + +class TestIpIrb(VppTestCase): + """IRB Test Case""" + + @classmethod + def setUpClass(cls): + """ + #. Create BD with MAC learning enabled and put interfaces to this BD. + #. Configure IPv4 addresses on loopback interface and routed interface. + #. Configure MAC address binding to IPv4 neighbors on loop0. + #. Configure MAC address on pg2. + #. Loopback BVI interface has remote hosts, one half of hosts are + behind pg0 second behind pg1. + """ + super(TestIpIrb, cls).setUpClass() + + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] # packet sizes + cls.bd_id = 10 + cls.remote_hosts_count = 250 + + # create 3 pg interfaces, 1 loopback interface + cls.create_pg_interfaces(range(3)) + cls.create_loopback_interfaces(range(1)) + + cls.interfaces = list(cls.pg_interfaces) + cls.interfaces.extend(cls.lo_interfaces) + + for i in cls.interfaces: + i.admin_up() + + # Create BD with MAC learning enabled and put interfaces to this BD + cls.vapi.sw_interface_set_l2_bridge( + cls.loop0.sw_if_index, bd_id=cls.bd_id, bvi=1) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg0.sw_if_index, bd_id=cls.bd_id) + cls.vapi.sw_interface_set_l2_bridge( + cls.pg1.sw_if_index, bd_id=cls.bd_id) + + # Configure IPv4 addresses on loopback interface and routed interface + cls.loop0.config_ip4() + cls.pg2.config_ip4() + + # Configure MAC address binding to IPv4 neighbors on loop0 + cls.loop0.generate_remote_hosts(cls.remote_hosts_count) + cls.loop0.configure_ipv4_neighbors() + # configure MAC address on pg2 + cls.pg2.resolve_arp() + + # Loopback BVI interface has remote hosts, one half of hosts are behind + # pg0 second behind pg1 + half = cls.remote_hosts_count // 2 + cls.pg0.remote_hosts = cls.loop0.remote_hosts[:half] + cls.pg1.remote_hosts = cls.loop0.remote_hosts[half:] + + def tearDown(self): + """Run standard test teardown and log ``show l2patch``, + ``show l2fib verbose``,``show bridge-domain <bd_id> detail``, + ``show ip arp``. + """ + super(TestIpIrb, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show l2patch")) + self.logger.info(self.vapi.cli("show l2fib verbose")) + self.logger.info(self.vapi.cli("show bridge-domain %s detail" % + self.bd_id)) + self.logger.info(self.vapi.cli("show ip arp")) + + def create_stream(self, src_ip_if, dst_ip_if, packet_sizes): + pkts = [] + for i in range(0, 257): + remote_dst_host = choice(dst_ip_if.remote_hosts) + info = self.create_packet_info(src_ip_if, dst_ip_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_ip_if.local_mac, src=src_ip_if.remote_mac) / + IP(src=src_ip_if.remote_ip4, + dst=remote_dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def create_stream_l2_to_ip(self, src_l2_if, src_ip_if, dst_ip_if, + packet_sizes): + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_ip_if, dst_ip_if) + payload = self.info_to_payload(info) + + host = choice(src_l2_if.remote_hosts) + + p = (Ether(src=host.mac, + dst=src_ip_if.local_mac) / + IP(src=host.ip4, + dst=dst_ip_if.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + + info.data = p.copy() + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + + pkts.append(p) + return pkts + + def verify_capture_l2_to_ip(self, dst_ip_if, src_ip_if, capture): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + + dst_ip_sw_if_index = dst_ip_if.sw_if_index + + for packet in capture: + ip = packet[IP] + udp = packet[IP][UDP] + payload_info = self.payload_to_info(str(packet[IP][UDP][Raw])) + + self.assertEqual(payload_info.dst, dst_ip_sw_if_index) + + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_ip_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + saved_packet = next_info.data + self.assertTrue(next_info is not None) + + # MAC: src, dst + self.assertEqual(packet.src, dst_ip_if.local_mac) + self.assertEqual(packet.dst, dst_ip_if.remote_mac) + + # IP: src, dst + host = src_ip_if.host_by_ip4(ip.src) + self.assertIsNotNone(host) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(ip.dst, dst_ip_if.remote_ip4) + + # UDP: + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + + def verify_capture(self, dst_ip_if, src_ip_if, capture): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + + dst_ip_sw_if_index = dst_ip_if.sw_if_index + + for packet in capture: + ip = packet[IP] + udp = packet[IP][UDP] + payload_info = self.payload_to_info(str(packet[IP][UDP][Raw])) + packet_index = payload_info.index + + self.assertEqual(payload_info.dst, dst_ip_sw_if_index) + + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_ip_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + self.assertTrue(next_info is not None) + + # MAC: src, dst + self.assertEqual(packet.src, dst_ip_if.local_mac) + host = dst_ip_if.host_by_mac(packet.dst) + + # IP: src, dst + self.assertEqual(ip.src, src_ip_if.remote_ip4) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(ip.dst, host.ip4) + + # UDP: + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + + def test_ip4_irb_1(self): + """ IPv4 IRB test 1 + + Test scenario: + - ip traffic from pg2 interface must ends in both pg0 and pg1 + - arp entry present in loop0 interface for destination IP + - no l2 entree configured, pg0 and pg1 are same + """ + + stream = self.create_stream( + self.pg2, self.loop0, self.pg_if_packet_sizes) + self.pg2.add_stream(stream) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + packet_count = self.get_packet_count_for_if_idx(self.loop0.sw_if_index) + + rcvd1 = self.pg0.get_capture(packet_count) + rcvd2 = self.pg1.get_capture(packet_count) + + self.verify_capture(self.loop0, self.pg2, rcvd1) + self.verify_capture(self.loop0, self.pg2, rcvd2) + + self.assertListEqual(rcvd1.res, rcvd2.res) + + def test_ip4_irb_2(self): + """ IPv4 IRB test 2 + + Test scenario: + - ip traffic from pg0 and pg1 ends on pg2 + """ + + stream1 = self.create_stream_l2_to_ip( + self.pg0, self.loop0, self.pg2, self.pg_if_packet_sizes) + stream2 = self.create_stream_l2_to_ip( + self.pg1, self.loop0, self.pg2, self.pg_if_packet_sizes) + self.pg0.add_stream(stream1) + self.pg1.add_stream(stream2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rcvd = self.pg2.get_capture() + self.verify_capture_l2_to_ip(self.pg2, self.loop0, rcvd) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip4_vrf_multi_instance.py b/test/test_ip4_vrf_multi_instance.py new file mode 100644 index 00000000..5a8d6760 --- /dev/null +++ b/test/test_ip4_vrf_multi_instance.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python +"""IP4 VRF Multi-instance Test Case HLD: + +**NOTES:** + - higher number of pg-ip4 interfaces causes problems => only 15 pg-ip4 \ + interfaces in 5 VRFs are tested + - jumbo packets in configuration with 15 pg-ip4 interfaces leads to \ + problems too + - Reset of FIB table / VRF does not remove routes from IP FIB (see Jira \ + ticket https://jira.fd.io/browse/VPP-560) so checks of reset VRF tables \ + are skipped in tests 2, 3 and 4 + +**config 1** + - add 15 pg-ip4 interfaces + - configure 5 hosts per pg-ip4 interface + - configure 4 VRFs + - add 3 pg-ip4 interfaces per VRF + +**test 1** + - send IP4 packets between all pg-ip4 interfaces in all VRF groups + +**verify 1** + - check VRF data by parsing output of ip_fib_dump API command + - all packets received correctly in case of pg-ip4 interfaces in VRF + - no packet received in case of pg-ip4 interfaces not in VRF + +**config 2** + - delete 2 VRFs + +**test 2** + - send IP4 packets between all pg-ip4 interfaces in all VRF groups + +**verify 2** + - check VRF data by parsing output of ip_fib_dump API command + - all packets received correctly in case of pg-ip4 interfaces in VRF + - no packet received in case of pg-ip4 interfaces not in VRF + +**config 3** + - add 1 of deleted VRFs and 1 new VRF + +**test 3** + - send IP4 packets between all pg-ip4 interfaces in all VRF groups + +**verify 3** + - check VRF data by parsing output of ip_fib_dump API command + - all packets received correctly in case of pg-ip4 interfaces in VRF + - no packet received in case of pg-ip4 interfaces not in VRF + +**config 4** + - delete all VRFs (i.e. no VRF except VRF=0 created) + +**test 4** + - send IP4 packets between all pg-ip4 interfaces in all VRF groups + +**verify 4** + - check VRF data by parsing output of ip_fib_dump API command + - all packets received correctly in case of pg-ip4 interfaces in VRF + - no packet received in case of pg-ip4 interfaces not in VRF +""" + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP, ARP + +from framework import VppTestCase, VppTestRunner +from util import ppp + + +def is_ipv4_misc(p): + """ Is packet one of uninteresting IPv4 broadcasts? """ + if p.haslayer(ARP): + return True + return False + + +class TestIp4VrfMultiInst(VppTestCase): + """ IP4 VRF Multi-instance Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestIp4VrfMultiInst, cls).setUpClass() + + # Test variables + cls.hosts_per_pg = 5 + cls.nr_of_vrfs = 5 + cls.pg_ifs_per_vrf = 3 + + try: + # Create pg interfaces + cls.create_pg_interfaces( + range(cls.nr_of_vrfs * cls.pg_ifs_per_vrf)) + + # Packet flows mapping pg0 -> pg1, pg2 etc. + cls.flows = dict() + for i in range(len(cls.pg_interfaces)): + multiplicand = i / cls.pg_ifs_per_vrf + pg_list = [ + cls.pg_interfaces[multiplicand * cls.pg_ifs_per_vrf + j] + for j in range(cls.pg_ifs_per_vrf) + if (multiplicand * cls.pg_ifs_per_vrf + j) != i] + cls.flows[cls.pg_interfaces[i]] = pg_list + + # Packet sizes - jumbo packet (9018 bytes) skipped + cls.pg_if_packet_sizes = [64, 512, 1518] + + # Set up all interfaces + for pg_if in cls.pg_interfaces: + pg_if.admin_up() + pg_if.generate_remote_hosts(cls.hosts_per_pg) + + # Create list of VRFs + cls.vrf_list = list() + + # Create list of deleted VRFs + cls.vrf_deleted_list = list() + + # Create list of pg_interfaces in VRFs + cls.pg_in_vrf = list() + + # Create list of pg_interfaces not in BDs + cls.pg_not_in_vrf = [pg_if for pg_if in cls.pg_interfaces] + + # Create mapping of pg_interfaces to VRF IDs + cls.pg_if_by_vrf_id = dict() + for i in range(cls.nr_of_vrfs): + vrf_id = i + 1 + pg_list = [ + cls.pg_interfaces[i * cls.pg_ifs_per_vrf + j] + for j in range(cls.pg_ifs_per_vrf)] + cls.pg_if_by_vrf_id[vrf_id] = pg_list + + except Exception: + super(TestIp4VrfMultiInst, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + super(TestIp4VrfMultiInst, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestIp4VrfMultiInst, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show ip fib")) + self.logger.info(self.vapi.ppcli("show ip arp")) + + def create_vrf_and_assign_interfaces(self, count, start=1): + """ + Create required number of FIB tables / VRFs, put 3 l2-pg interfaces + to every FIB table / VRF. + + :param int count: Number of FIB tables / VRFs to be created. + :param int start: Starting number of the FIB table / VRF ID. \ + (Default value = 1) + """ + + for i in range(count): + vrf_id = i + start + pg_if = self.pg_if_by_vrf_id[vrf_id][0] + dest_addr = pg_if.remote_hosts[0].ip4n + dest_addr_len = 24 + self.vapi.ip_table_add_del(vrf_id, is_add=1) + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, pg_if.local_ip4n, + table_id=vrf_id, is_multipath=1) + self.logger.info("IPv4 VRF ID %d created" % vrf_id) + if vrf_id not in self.vrf_list: + self.vrf_list.append(vrf_id) + if vrf_id in self.vrf_deleted_list: + self.vrf_deleted_list.remove(vrf_id) + for j in range(self.pg_ifs_per_vrf): + pg_if = self.pg_if_by_vrf_id[vrf_id][j] + pg_if.set_table_ip4(vrf_id) + self.logger.info("pg-interface %s added to IPv4 VRF ID %d" + % (pg_if.name, vrf_id)) + if pg_if not in self.pg_in_vrf: + self.pg_in_vrf.append(pg_if) + if pg_if in self.pg_not_in_vrf: + self.pg_not_in_vrf.remove(pg_if) + pg_if.config_ip4() + pg_if.configure_ipv4_neighbors() + self.logger.debug(self.vapi.ppcli("show ip fib")) + self.logger.debug(self.vapi.ppcli("show ip arp")) + + def delete_vrf(self, vrf_id): + """ + Delete required FIB table / VRF. + + :param int vrf_id: The FIB table / VRF ID to be deleted. + """ + # self.vapi.reset_vrf(vrf_id, is_ipv6=0) + self.vapi.reset_fib(vrf_id, is_ipv6=0) + if vrf_id in self.vrf_list: + self.vrf_list.remove(vrf_id) + if vrf_id not in self.vrf_deleted_list: + self.vrf_deleted_list.append(vrf_id) + for j in range(self.pg_ifs_per_vrf): + pg_if = self.pg_if_by_vrf_id[vrf_id][j] + pg_if.unconfig_ip4() + if pg_if in self.pg_in_vrf: + self.pg_in_vrf.remove(pg_if) + if pg_if not in self.pg_not_in_vrf: + self.pg_not_in_vrf.append(pg_if) + self.logger.info("IPv4 VRF ID %d reset" % vrf_id) + self.logger.debug(self.vapi.ppcli("show ip fib")) + self.logger.debug(self.vapi.ppcli("show ip arp")) + self.vapi.ip_table_add_del(vrf_id, is_add=0) + + def create_stream(self, src_if, packet_sizes): + """ + Create input packet stream for defined interface using hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :return: Stream of packets. + """ + pkts = [] + src_hosts = src_if.remote_hosts + for dst_if in self.flows[src_if]: + for dst_host in dst_if.remote_hosts: + src_host = random.choice(src_hosts) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=src_if.local_mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + self.logger.debug("Input stream created for port %s. Length: %u pkt(s)" + % (src_if.name, len(pkts))) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + try: + ip = packet[IP] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertIsNotNone(next_info) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertIsNone( + remaining_packet, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def verify_vrf(self, vrf_id): + """ + Check if the FIB table / VRF ID is configured. + + :param int vrf_id: The FIB table / VRF ID to be verified. + :return: 1 if the FIB table / VRF ID is configured, otherwise return 0. + """ + ip_fib_dump = self.vapi.ip_fib_dump() + vrf_count = 0 + for ip_fib_details in ip_fib_dump: + if ip_fib_details[2] == vrf_id: + vrf_count += 1 + if vrf_count == 0: + self.logger.info("IPv4 VRF ID %d is not configured" % vrf_id) + return 0 + else: + self.logger.info("IPv4 VRF ID %d is configured" % vrf_id) + return 1 + + def run_verify_test(self): + """ + Create packet streams for all configured l2-pg interfaces, send all \ + prepared packet streams and verify that: + - all packets received correctly on all pg-l2 interfaces assigned + to bridge domains + - no packet received on all pg-l2 interfaces not assigned to bridge + domains + + :raise RuntimeError: If no packet captured on l2-pg interface assigned + to the bridge domain or if any packet is captured on l2-pg + interface not assigned to the bridge domain. + """ + # Test + # Create incoming packet streams for packet-generator interfaces + for pg_if in self.pg_interfaces: + pkts = self.create_stream(pg_if, self.pg_if_packet_sizes) + pg_if.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for pg_if in self.pg_interfaces: + if pg_if in self.pg_in_vrf: + capture = pg_if.get_capture(remark="interface is in VRF") + self.verify_capture(pg_if, capture) + elif pg_if in self.pg_not_in_vrf: + pg_if.assert_nothing_captured(remark="interface is not in VRF", + filter_out_fn=is_ipv4_misc) + self.logger.debug("No capture for interface %s" % pg_if.name) + else: + raise Exception("Unknown interface: %s" % pg_if.name) + + def test_ip4_vrf_01(self): + """ IP4 VRF Multi-instance test 1 - create 5 BDs + """ + # Config 1 + # Create 4 VRFs + self.create_vrf_and_assign_interfaces(4) + + # Verify 1 + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), 1) + + # Test 1 + self.run_verify_test() + + def test_ip4_vrf_02(self): + """ IP4 VRF Multi-instance test 2 - delete 2 VRFs + """ + # Config 2 + # Delete 2 VRFs + self.delete_vrf(1) + self.delete_vrf(2) + + # Verify 2 + # for vrf_id in self.vrf_deleted_list: + # self.assertEqual(self.verify_vrf(vrf_id), 0) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), 1) + + # Test 2 + self.run_verify_test() + + def test_ip4_vrf_03(self): + """ IP4 VRF Multi-instance 3 - add 2 VRFs + """ + # Config 3 + # Add 1 of deleted VRFs and 1 new VRF + self.create_vrf_and_assign_interfaces(1) + self.create_vrf_and_assign_interfaces(1, start=5) + + # Verify 3 + # for vrf_id in self.vrf_deleted_list: + # self.assertEqual(self.verify_vrf(vrf_id), 0) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), 1) + + # Test 3 + self.run_verify_test() + + def test_ip4_vrf_04(self): + """ IP4 VRF Multi-instance test 4 - delete 4 VRFs + """ + # Config 4 + # Delete all VRFs (i.e. no VRF except VRF=0 created) + for i in range(len(self.vrf_list)): + self.delete_vrf(self.vrf_list[0]) + + # Verify 4 + # for vrf_id in self.vrf_deleted_list: + # self.assertEqual(self.verify_vrf(vrf_id), 0) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), 1) + + # Test 4 + self.run_verify_test() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip6.py b/test/test_ip6.py new file mode 100644 index 00000000..aad3713c --- /dev/null +++ b/test/test_ip6.py @@ -0,0 +1,1510 @@ +#!/usr/bin/env python + +import unittest +from socket import AF_INET6 + +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppSubInterface, VppDot1QSubint +from vpp_pg_interface import is_ipv6_misc +from vpp_ip_route import VppIpRoute, VppRoutePath, find_route, VppIpMRoute, \ + VppMRoutePath, MRouteItfFlags, MRouteEntryFlags, VppMplsIpBind, \ + VppMplsRoute, DpoProto, VppMplsTable +from vpp_neighbor import find_nbr, VppNeighbor + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q +from scapy.layers.inet6 import IPv6, UDP, ICMPv6ND_NS, ICMPv6ND_RS, \ + ICMPv6ND_RA, ICMPv6NDOptSrcLLAddr, getmacbyip6, ICMPv6MRD_Solicitation, \ + ICMPv6NDOptMTU, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptPrefixInfo, \ + ICMPv6ND_NA, ICMPv6NDOptDstLLAddr, ICMPv6DestUnreach, icmp6types + +from util import ppp +from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ptop, in6_islladdr, \ + in6_mactoifaceid, in6_ismaddr +from scapy.utils import inet_pton, inet_ntop +from scapy.contrib.mpls import MPLS + + +def mk_ll_addr(mac): + euid = in6_mactoifaceid(mac) + addr = "fe80::" + euid + return addr + + +class TestIPv6ND(VppTestCase): + def validate_ra(self, intf, rx, dst_ip=None): + if not dst_ip: + dst_ip = intf.remote_ip6 + + # unicasted packets must come to the unicast mac + self.assertEqual(rx[Ether].dst, intf.remote_mac) + + # and from the router's MAC + self.assertEqual(rx[Ether].src, intf.local_mac) + + # the rx'd RA should be addressed to the sender's source + self.assertTrue(rx.haslayer(ICMPv6ND_RA)) + self.assertEqual(in6_ptop(rx[IPv6].dst), + in6_ptop(dst_ip)) + + # and come from the router's link local + self.assertTrue(in6_islladdr(rx[IPv6].src)) + self.assertEqual(in6_ptop(rx[IPv6].src), + in6_ptop(mk_ll_addr(intf.local_mac))) + + def validate_na(self, intf, rx, dst_ip=None, tgt_ip=None): + if not dst_ip: + dst_ip = intf.remote_ip6 + if not tgt_ip: + dst_ip = intf.local_ip6 + + # unicasted packets must come to the unicast mac + self.assertEqual(rx[Ether].dst, intf.remote_mac) + + # and from the router's MAC + self.assertEqual(rx[Ether].src, intf.local_mac) + + # the rx'd NA should be addressed to the sender's source + self.assertTrue(rx.haslayer(ICMPv6ND_NA)) + self.assertEqual(in6_ptop(rx[IPv6].dst), + in6_ptop(dst_ip)) + + # and come from the target address + self.assertEqual(in6_ptop(rx[IPv6].src), in6_ptop(tgt_ip)) + + # Dest link-layer options should have the router's MAC + dll = rx[ICMPv6NDOptDstLLAddr] + self.assertEqual(dll.lladdr, intf.local_mac) + + def validate_ns(self, intf, rx, tgt_ip): + nsma = in6_getnsma(inet_pton(AF_INET6, tgt_ip)) + dst_ip = inet_ntop(AF_INET6, nsma) + + # NS is broadcast + self.assertEqual(rx[Ether].dst, "ff:ff:ff:ff:ff:ff") + + # and from the router's MAC + self.assertEqual(rx[Ether].src, intf.local_mac) + + # the rx'd NS should be addressed to an mcast address + # derived from the target address + self.assertEqual(in6_ptop(rx[IPv6].dst), in6_ptop(dst_ip)) + + # expect the tgt IP in the NS header + ns = rx[ICMPv6ND_NS] + self.assertEqual(in6_ptop(ns.tgt), in6_ptop(tgt_ip)) + + # packet is from the router's local address + self.assertEqual(in6_ptop(rx[IPv6].src), intf.local_ip6) + + # Src link-layer options should have the router's MAC + sll = rx[ICMPv6NDOptSrcLLAddr] + self.assertEqual(sll.lladdr, intf.local_mac) + + def send_and_expect_ra(self, intf, pkts, remark, dst_ip=None, + filter_out_fn=is_ipv6_misc): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = intf.get_capture(1, filter_out_fn=filter_out_fn) + + self.assertEqual(len(rx), 1) + rx = rx[0] + self.validate_ra(intf, rx, dst_ip) + + def send_and_expect_na(self, intf, pkts, remark, dst_ip=None, + tgt_ip=None, + filter_out_fn=is_ipv6_misc): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = intf.get_capture(1, filter_out_fn=filter_out_fn) + + self.assertEqual(len(rx), 1) + rx = rx[0] + self.validate_na(intf, rx, dst_ip, tgt_ip) + + def send_and_expect_ns(self, tx_intf, rx_intf, pkts, tgt_ip, + filter_out_fn=is_ipv6_misc): + tx_intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = rx_intf.get_capture(1, filter_out_fn=filter_out_fn) + + self.assertEqual(len(rx), 1) + rx = rx[0] + self.validate_ns(rx_intf, rx, tgt_ip) + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) + + def verify_ip(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + ip = rx[IPv6] + self.assertEqual(ip.src, sip) + self.assertEqual(ip.dst, dip) + + +class TestIPv6(TestIPv6ND): + """ IPv6 Test Case """ + + @classmethod + def setUpClass(cls): + super(TestIPv6, cls).setUpClass() + + def setUp(self): + """ + Perform test setup before test case. + + **Config:** + - create 3 pg interfaces + - untagged pg0 interface + - Dot1Q subinterface on pg1 + - Dot1AD subinterface on pg2 + - setup interfaces: + - put it into UP state + - set IPv6 addresses + - resolve neighbor address using NDP + - configure 200 fib entries + + :ivar list interfaces: pg interfaces and subinterfaces. + :ivar dict flows: IPv4 packet flows in test. + :ivar list pg_if_packet_sizes: packet sizes in test. + + *TODO:* Create AD sub interface + """ + super(TestIPv6, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) + + # create 2 subinterfaces for p1 and pg2 + self.sub_interfaces = [ + VppDot1QSubint(self, self.pg1, 100), + VppDot1QSubint(self, self.pg2, 200) + # TODO: VppDot1ADSubint(self, self.pg2, 200, 300, 400) + ] + + # packet flows mapping pg0 -> pg1.sub, pg2.sub, etc. + self.flows = dict() + self.flows[self.pg0] = [self.pg1.sub_if, self.pg2.sub_if] + self.flows[self.pg1.sub_if] = [self.pg0, self.pg2.sub_if] + self.flows[self.pg2.sub_if] = [self.pg0, self.pg1.sub_if] + + # packet sizes + self.pg_if_packet_sizes = [64, 512, 1518, 9018] + self.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] + + self.interfaces = list(self.pg_interfaces) + self.interfaces.extend(self.sub_interfaces) + + # setup all interfaces + for i in self.interfaces: + i.admin_up() + i.config_ip6() + i.resolve_ndp() + + # config 2M FIB entries + self.config_fib_entries(200) + + def tearDown(self): + """Run standard test teardown and log ``show ip6 neighbors``.""" + for i in self.sub_interfaces: + i.unconfig_ip6() + i.ip6_disable() + i.admin_down() + i.remove_vpp_config() + + super(TestIPv6, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show ip6 neighbors")) + # info(self.vapi.cli("show ip6 fib")) # many entries + + def config_fib_entries(self, count): + """For each interface add to the FIB table *count* routes to + "fd02::1/128" destination with interface's local address as next-hop + address. + + :param int count: Number of FIB entries. + + - *TODO:* check if the next-hop address shouldn't be remote address + instead of local address. + """ + n_int = len(self.interfaces) + percent = 0 + counter = 0.0 + dest_addr = inet_pton(AF_INET6, "fd02::1") + dest_addr_len = 128 + for i in self.interfaces: + next_hop_address = i.local_ip6n + for j in range(count / n_int): + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, next_hop_address, is_ipv6=1) + counter += 1 + if counter / count * 100 > percent: + self.logger.info("Configure %d FIB entries .. %d%% done" % + (count, percent)) + percent += 1 + + def create_stream(self, src_if, packet_sizes): + """Create input packet stream for defined interface. + + :param VppInterface src_if: Interface to create packet stream for. + :param list packet_sizes: Required packet sizes. + """ + pkts = [] + for i in range(0, 257): + dst_if = self.flows[src_if][i % 2] + info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + if isinstance(src_if, VppSubInterface): + p = src_if.add_dot1_layer(p) + size = packet_sizes[(i // 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, dst_if, capture): + """Verify captured input packet stream for defined interface. + + :param VppInterface dst_if: Interface to verify captured packet stream + for. + :param list capture: Captured packet stream. + """ + self.logger.info("Verifying capture on interface %s" % dst_if.name) + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + is_sub_if = False + dst_sw_if_index = dst_if.sw_if_index + if hasattr(dst_if, 'parent'): + is_sub_if = True + for packet in capture: + if is_sub_if: + # Check VLAN tags and Ethernet header + packet = dst_if.remove_dot1_layer(packet) + self.assertTrue(Dot1Q not in packet) + try: + ip = packet[IPv6] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug( + "Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IPv6].src) + self.assertEqual(ip.dst, saved_packet[IPv6].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) + + def test_fib(self): + """ IPv6 FIB test + + Test scenario: + - Create IPv6 stream for pg0 interface + - Create IPv6 tagged streams for pg1's and pg2's subinterface. + - Send and verify received packets on each interface. + """ + + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + for i in self.sub_interfaces: + pkts = self.create_stream(i, self.sub_if_packet_sizes) + i.parent.add_stream(pkts) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + pkts = self.pg0.get_capture() + self.verify_capture(self.pg0, pkts) + + for i in self.sub_interfaces: + pkts = i.parent.get_capture() + self.verify_capture(i, pkts) + + def test_ns(self): + """ IPv6 Neighbour Solicitation Exceptions + + Test scenario: + - Send an NS Sourced from an address not covered by the link sub-net + - Send an NS to an mcast address the router has not joined + - Send NS for a target address the router does not onn. + """ + + # + # An NS from a non link source address + # + nsma = in6_getnsma(inet_pton(AF_INET6, self.pg0.local_ip6)) + d = inet_ntop(AF_INET6, nsma) + + p = (Ether(dst=in6_getnsmac(nsma)) / + IPv6(dst=d, src="2002::2") / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + pkts = [p] + + self.send_and_assert_no_replies( + self.pg0, pkts, + "No response to NS source by address not on sub-net") + + # + # An NS for sent to a solicited mcast group the router is + # not a member of FAILS + # + if 0: + nsma = in6_getnsma(inet_pton(AF_INET6, "fd::ffff")) + d = inet_ntop(AF_INET6, nsma) + + p = (Ether(dst=in6_getnsmac(nsma)) / + IPv6(dst=d, src=self.pg0.remote_ip6) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + pkts = [p] + + self.send_and_assert_no_replies( + self.pg0, pkts, + "No response to NS sent to unjoined mcast address") + + # + # An NS whose target address is one the router does not own + # + nsma = in6_getnsma(inet_pton(AF_INET6, self.pg0.local_ip6)) + d = inet_ntop(AF_INET6, nsma) + + p = (Ether(dst=in6_getnsmac(nsma)) / + IPv6(dst=d, src=self.pg0.remote_ip6) / + ICMPv6ND_NS(tgt="fd::ffff") / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + pkts = [p] + + self.send_and_assert_no_replies(self.pg0, pkts, + "No response to NS for unknown target") + + # + # A neighbor entry that has no associated FIB-entry + # + self.pg0.generate_remote_hosts(4) + nd_entry = VppNeighbor(self, + self.pg0.sw_if_index, + self.pg0.remote_hosts[2].mac, + self.pg0.remote_hosts[2].ip6, + af=AF_INET6, + is_no_fib_entry=1) + nd_entry.add_vpp_config() + + # + # check we have the neighbor, but no route + # + self.assertTrue(find_nbr(self, + self.pg0.sw_if_index, + self.pg0._remote_hosts[2].ip6, + inet=AF_INET6)) + self.assertFalse(find_route(self, + self.pg0._remote_hosts[2].ip6, + 128, + inet=AF_INET6)) + + # + # send an NS from a link local address to the interface's global + # address + # + p = (Ether(dst=in6_getnsmac(nsma), src=self.pg0.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[2].ip6_ll) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + + self.send_and_expect_na(self.pg0, p, + "NS from link-local", + dst_ip=self.pg0._remote_hosts[2].ip6_ll, + tgt_ip=self.pg0.local_ip6) + + # + # we should have learned an ND entry for the peer's link-local + # but not inserted a route to it in the FIB + # + self.assertTrue(find_nbr(self, + self.pg0.sw_if_index, + self.pg0._remote_hosts[2].ip6_ll, + inet=AF_INET6)) + self.assertFalse(find_route(self, + self.pg0._remote_hosts[2].ip6_ll, + 128, + inet=AF_INET6)) + + # + # An NS to the router's own Link-local + # + p = (Ether(dst=in6_getnsmac(nsma), src=self.pg0.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[3].ip6_ll) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6_ll) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + + self.send_and_expect_na(self.pg0, p, + "NS to/from link-local", + dst_ip=self.pg0._remote_hosts[3].ip6_ll, + tgt_ip=self.pg0.local_ip6_ll) + + # + # we should have learned an ND entry for the peer's link-local + # but not inserted a route to it in the FIB + # + self.assertTrue(find_nbr(self, + self.pg0.sw_if_index, + self.pg0._remote_hosts[3].ip6_ll, + inet=AF_INET6)) + self.assertFalse(find_route(self, + self.pg0._remote_hosts[3].ip6_ll, + 128, + inet=AF_INET6)) + + def test_ns_duplicates(self): + """ ND Duplicates""" + + # + # Generate some hosts on the LAN + # + self.pg1.generate_remote_hosts(3) + + # + # Add host 1 on pg1 and pg2 + # + ns_pg1 = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip6, + af=AF_INET6) + ns_pg1.add_vpp_config() + ns_pg2 = VppNeighbor(self, + self.pg2.sw_if_index, + self.pg2.remote_mac, + self.pg1.remote_hosts[1].ip6, + af=AF_INET6) + ns_pg2.add_vpp_config() + + # + # IP packet destined for pg1 remote host arrives on pg1 again. + # + p = (Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + IPv6(src=self.pg0.remote_ip6, + dst=self.pg1.remote_hosts[1].ip6) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_ip(rx1[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip6, + self.pg1.remote_hosts[1].ip6) + + # + # remove the duplicate on pg1 + # packet stream shoud generate NSs out of pg1 + # + ns_pg1.remove_vpp_config() + + self.send_and_expect_ns(self.pg0, self.pg1, + p, self.pg1.remote_hosts[1].ip6) + + # + # Add it back + # + ns_pg1.add_vpp_config() + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_ip(rx1[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip6, + self.pg1.remote_hosts[1].ip6) + + def validate_ra(self, intf, rx, dst_ip=None, mtu=9000, pi_opt=None): + if not dst_ip: + dst_ip = intf.remote_ip6 + + # unicasted packets must come to the unicast mac + self.assertEqual(rx[Ether].dst, intf.remote_mac) + + # and from the router's MAC + self.assertEqual(rx[Ether].src, intf.local_mac) + + # the rx'd RA should be addressed to the sender's source + self.assertTrue(rx.haslayer(ICMPv6ND_RA)) + self.assertEqual(in6_ptop(rx[IPv6].dst), + in6_ptop(dst_ip)) + + # and come from the router's link local + self.assertTrue(in6_islladdr(rx[IPv6].src)) + self.assertEqual(in6_ptop(rx[IPv6].src), + in6_ptop(mk_ll_addr(intf.local_mac))) + + # it should contain the links MTU + ra = rx[ICMPv6ND_RA] + self.assertEqual(ra[ICMPv6NDOptMTU].mtu, mtu) + + # it should contain the source's link layer address option + sll = ra[ICMPv6NDOptSrcLLAddr] + self.assertEqual(sll.lladdr, intf.local_mac) + + if not pi_opt: + # the RA should not contain prefix information + self.assertFalse(ra.haslayer(ICMPv6NDOptPrefixInfo)) + else: + raos = rx.getlayer(ICMPv6NDOptPrefixInfo, 1) + + # the options are nested in the scapy packet in way that i cannot + # decipher how to decode. this 1st layer of option always returns + # nested classes, so a direct obj1=obj2 comparison always fails. + # however, the getlayer(.., 2) does give one instnace. + # so we cheat here and construct a new opt instnace for comparison + rd = ICMPv6NDOptPrefixInfo(prefixlen=raos.prefixlen, + prefix=raos.prefix, + L=raos.L, + A=raos.A) + if type(pi_opt) is list: + for ii in range(len(pi_opt)): + self.assertEqual(pi_opt[ii], rd) + rd = rx.getlayer(ICMPv6NDOptPrefixInfo, ii+2) + else: + self.assertEqual(pi_opt, raos) + + def send_and_expect_ra(self, intf, pkts, remark, dst_ip=None, + filter_out_fn=is_ipv6_misc, + opt=None): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = intf.get_capture(1, filter_out_fn=filter_out_fn) + + self.assertEqual(len(rx), 1) + rx = rx[0] + self.validate_ra(intf, rx, dst_ip, pi_opt=opt) + + def test_rs(self): + """ IPv6 Router Solicitation Exceptions + + Test scenario: + """ + + # + # Before we begin change the IPv6 RA responses to use the unicast + # address - that way we will not confuse them with the periodic + # RAs which go to the mcast address + # Sit and wait for the first periodic RA. + # + # TODO + # + self.pg0.ip6_ra_config(send_unicast=1) + + # + # An RS from a link source address + # - expect an RA in return + # + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) / + ICMPv6ND_RS()) + pkts = [p] + self.send_and_expect_ra(self.pg0, pkts, "Genuine RS") + + # + # For the next RS sent the RA should be rate limited + # + self.send_and_assert_no_replies(self.pg0, pkts, "RA rate limited") + + # + # When we reconfiure the IPv6 RA config, we reset the RA rate limiting, + # so we need to do this before each test below so as not to drop + # packets for rate limiting reasons. Test this works here. + # + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, pkts, "Rate limit reset RS") + + # + # An RS sent from a non-link local source + # + self.pg0.ip6_ra_config(send_unicast=1) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src="2002::ffff") / + ICMPv6ND_RS()) + pkts = [p] + self.send_and_assert_no_replies(self.pg0, pkts, + "RS from non-link source") + + # + # Source an RS from a link local address + # + self.pg0.ip6_ra_config(send_unicast=1) + ll = mk_ll_addr(self.pg0.remote_mac) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=ll) / + ICMPv6ND_RS()) + pkts = [p] + self.send_and_expect_ra(self.pg0, pkts, + "RS sourced from link-local", + dst_ip=ll) + + # + # Send the RS multicast + # + self.pg0.ip6_ra_config(send_unicast=1) + dmac = in6_getnsmac(inet_pton(AF_INET6, "ff02::2")) + ll = mk_ll_addr(self.pg0.remote_mac) + p = (Ether(dst=dmac, src=self.pg0.remote_mac) / + IPv6(dst="ff02::2", src=ll) / + ICMPv6ND_RS()) + pkts = [p] + self.send_and_expect_ra(self.pg0, pkts, + "RS sourced from link-local", + dst_ip=ll) + + # + # Source from the unspecified address ::. This happens when the RS + # is sent before the host has a configured address/sub-net, + # i.e. auto-config. Since the sender has no IP address, the reply + # comes back mcast - so the capture needs to not filter this. + # If we happen to pick up the periodic RA at this point then so be it, + # it's not an error. + # + self.pg0.ip6_ra_config(send_unicast=1, suppress=1) + p = (Ether(dst=dmac, src=self.pg0.remote_mac) / + IPv6(dst="ff02::2", src="::") / + ICMPv6ND_RS()) + pkts = [p] + self.send_and_expect_ra(self.pg0, pkts, + "RS sourced from unspecified", + dst_ip="ff02::1", + filter_out_fn=None) + + # + # Configure The RA to announce the links prefix + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len) + + # + # RAs should now contain the prefix information option + # + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=1, + A=1) + + self.pg0.ip6_ra_config(send_unicast=1) + ll = mk_ll_addr(self.pg0.remote_mac) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=ll) / + ICMPv6ND_RS()) + self.send_and_expect_ra(self.pg0, p, + "RA with prefix-info", + dst_ip=ll, + opt=opt) + + # + # Change the prefix info to not off-link + # L-flag is clear + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len, + off_link=1) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=0, + A=1) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix info with L-flag=0", + dst_ip=ll, + opt=opt) + + # + # Change the prefix info to not off-link, no-autoconfig + # L and A flag are clear in the advert + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len, + off_link=1, + no_autoconfig=1) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=0, + A=0) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix info with A & L-flag=0", + dst_ip=ll, + opt=opt) + + # + # Change the flag settings back to the defaults + # L and A flag are set in the advert + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=1, + A=1) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix info", + dst_ip=ll, + opt=opt) + + # + # Change the prefix info to not off-link, no-autoconfig + # L and A flag are clear in the advert + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len, + off_link=1, + no_autoconfig=1) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=0, + A=0) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix info with A & L-flag=0", + dst_ip=ll, + opt=opt) + + # + # Use the reset to defults option to revert to defaults + # L and A flag are clear in the advert + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len, + use_default=1) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=1, + A=1) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix reverted to defaults", + dst_ip=ll, + opt=opt) + + # + # Advertise Another prefix. With no L-flag/A-flag + # + self.pg0.ip6_ra_prefix(self.pg1.local_ip6n, + self.pg1.local_ip6_prefix_len, + off_link=1, + no_autoconfig=1) + + opt = [ICMPv6NDOptPrefixInfo(prefixlen=self.pg0.local_ip6_prefix_len, + prefix=self.pg0.local_ip6, + L=1, + A=1), + ICMPv6NDOptPrefixInfo(prefixlen=self.pg1.local_ip6_prefix_len, + prefix=self.pg1.local_ip6, + L=0, + A=0)] + + self.pg0.ip6_ra_config(send_unicast=1) + ll = mk_ll_addr(self.pg0.remote_mac) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0.local_ip6, src=ll) / + ICMPv6ND_RS()) + self.send_and_expect_ra(self.pg0, p, + "RA with multiple Prefix infos", + dst_ip=ll, + opt=opt) + + # + # Remove the first refix-info - expect the second is still in the + # advert + # + self.pg0.ip6_ra_prefix(self.pg0.local_ip6n, + self.pg0.local_ip6_prefix_len, + is_no=1) + + opt = ICMPv6NDOptPrefixInfo(prefixlen=self.pg1.local_ip6_prefix_len, + prefix=self.pg1.local_ip6, + L=0, + A=0) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix reverted to defaults", + dst_ip=ll, + opt=opt) + + # + # Remove the second prefix-info - expect no prefix-info i nthe adverts + # + self.pg0.ip6_ra_prefix(self.pg1.local_ip6n, + self.pg1.local_ip6_prefix_len, + is_no=1) + + self.pg0.ip6_ra_config(send_unicast=1) + self.send_and_expect_ra(self.pg0, p, + "RA with Prefix reverted to defaults", + dst_ip=ll) + + # + # Reset the periodic advertisements back to default values + # + self.pg0.ip6_ra_config(no=1, suppress=1, send_unicast=0) + + +class IPv6NDProxyTest(TestIPv6ND): + """ IPv6 ND ProxyTest Case """ + + def setUp(self): + super(IPv6NDProxyTest, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(3)) + + # pg0 is the master interface, with the configured subnet + self.pg0.admin_up() + self.pg0.config_ip6() + self.pg0.resolve_ndp() + + self.pg1.ip6_enable() + self.pg2.ip6_enable() + + def tearDown(self): + super(IPv6NDProxyTest, self).tearDown() + + def test_nd_proxy(self): + """ IPv6 Proxy ND """ + + # + # Generate some hosts in the subnet that we are proxying + # + self.pg0.generate_remote_hosts(8) + + nsma = in6_getnsma(inet_pton(AF_INET6, self.pg0.local_ip6)) + d = inet_ntop(AF_INET6, nsma) + + # + # Send an NS for one of those remote hosts on one of the proxy links + # expect no response since it's from an address that is not + # on the link that has the prefix configured + # + ns_pg1 = (Ether(dst=in6_getnsmac(nsma), src=self.pg1.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[2].ip6) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0._remote_hosts[2].mac)) + + self.send_and_assert_no_replies(self.pg1, ns_pg1, "Off link NS") + + # + # Add proxy support for the host + # + self.vapi.ip6_nd_proxy( + inet_pton(AF_INET6, self.pg0._remote_hosts[2].ip6), + self.pg1.sw_if_index) + + # + # try that NS again. this time we expect an NA back + # + self.send_and_expect_na(self.pg1, ns_pg1, + "NS to proxy entry", + dst_ip=self.pg0._remote_hosts[2].ip6, + tgt_ip=self.pg0.local_ip6) + + # + # ... and that we have an entry in the ND cache + # + self.assertTrue(find_nbr(self, + self.pg1.sw_if_index, + self.pg0._remote_hosts[2].ip6, + inet=AF_INET6)) + + # + # ... and we can route traffic to it + # + t = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(dst=self.pg0._remote_hosts[2].ip6, + src=self.pg0.remote_ip6) / + UDP(sport=10000, dport=20000) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(t) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + rx = rx[0] + + self.assertEqual(rx[Ether].dst, self.pg0._remote_hosts[2].mac) + self.assertEqual(rx[Ether].src, self.pg1.local_mac) + + self.assertEqual(rx[IPv6].src, t[IPv6].src) + self.assertEqual(rx[IPv6].dst, t[IPv6].dst) + + # + # Test we proxy for the host on the main interface + # + ns_pg0 = (Ether(dst=in6_getnsmac(nsma), src=self.pg0.remote_mac) / + IPv6(dst=d, src=self.pg0.remote_ip6) / + ICMPv6ND_NS(tgt=self.pg0._remote_hosts[2].ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0.remote_mac)) + + self.send_and_expect_na(self.pg0, ns_pg0, + "NS to proxy entry on main", + tgt_ip=self.pg0._remote_hosts[2].ip6, + dst_ip=self.pg0.remote_ip6) + + # + # Setup and resolve proxy for another host on another interface + # + ns_pg2 = (Ether(dst=in6_getnsmac(nsma), src=self.pg2.remote_mac) / + IPv6(dst=d, src=self.pg0._remote_hosts[3].ip6) / + ICMPv6ND_NS(tgt=self.pg0.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.pg0._remote_hosts[2].mac)) + + self.vapi.ip6_nd_proxy( + inet_pton(AF_INET6, self.pg0._remote_hosts[3].ip6), + self.pg2.sw_if_index) + + self.send_and_expect_na(self.pg2, ns_pg2, + "NS to proxy entry other interface", + dst_ip=self.pg0._remote_hosts[3].ip6, + tgt_ip=self.pg0.local_ip6) + + self.assertTrue(find_nbr(self, + self.pg2.sw_if_index, + self.pg0._remote_hosts[3].ip6, + inet=AF_INET6)) + + # + # hosts can communicate. pg2->pg1 + # + t2 = (Ether(dst=self.pg2.local_mac, + src=self.pg0.remote_hosts[3].mac) / + IPv6(dst=self.pg0._remote_hosts[2].ip6, + src=self.pg0._remote_hosts[3].ip6) / + UDP(sport=10000, dport=20000) / + Raw('\xa5' * 100)) + + self.pg2.add_stream(t2) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + rx = rx[0] + + self.assertEqual(rx[Ether].dst, self.pg0._remote_hosts[2].mac) + self.assertEqual(rx[Ether].src, self.pg1.local_mac) + + self.assertEqual(rx[IPv6].src, t2[IPv6].src) + self.assertEqual(rx[IPv6].dst, t2[IPv6].dst) + + # + # remove the proxy configs + # + self.vapi.ip6_nd_proxy( + inet_pton(AF_INET6, self.pg0._remote_hosts[2].ip6), + self.pg1.sw_if_index, + is_del=1) + self.vapi.ip6_nd_proxy( + inet_pton(AF_INET6, self.pg0._remote_hosts[3].ip6), + self.pg2.sw_if_index, + is_del=1) + + self.assertFalse(find_nbr(self, + self.pg2.sw_if_index, + self.pg0._remote_hosts[3].ip6, + inet=AF_INET6)) + self.assertFalse(find_nbr(self, + self.pg1.sw_if_index, + self.pg0._remote_hosts[2].ip6, + inet=AF_INET6)) + + # + # no longer proxy-ing... + # + self.send_and_assert_no_replies(self.pg0, ns_pg0, "Proxy unconfigured") + self.send_and_assert_no_replies(self.pg1, ns_pg1, "Proxy unconfigured") + self.send_and_assert_no_replies(self.pg2, ns_pg2, "Proxy unconfigured") + + # + # no longer forwarding. traffic generates NS out of the glean/main + # interface + # + self.pg2.add_stream(t2) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + + self.assertTrue(rx[0].haslayer(ICMPv6ND_NS)) + + +class TestIPNull(VppTestCase): + """ IPv6 routes via NULL """ + + def setUp(self): + super(TestIPNull, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(1)) + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip6() + i.resolve_ndp() + + def tearDown(self): + super(TestIPNull, self).tearDown() + for i in self.pg_interfaces: + i.unconfig_ip6() + i.admin_down() + + def test_ip_null(self): + """ IP NULL route """ + + p = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IPv6(src=self.pg0.remote_ip6, dst="2001::1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # A route via IP NULL that will reply with ICMP unreachables + # + ip_unreach = VppIpRoute(self, "2001::", 64, [], is_unreach=1, is_ip6=1) + ip_unreach.add_vpp_config() + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + rx = rx[0] + icmp = rx[ICMPv6DestUnreach] + + # 0 = "No route to destination" + self.assertEqual(icmp.code, 0) + + # ICMP is rate limited. pause a bit + self.sleep(1) + + # + # A route via IP NULL that will reply with ICMP prohibited + # + ip_prohibit = VppIpRoute(self, "2001::1", 128, [], + is_prohibit=1, is_ip6=1) + ip_prohibit.add_vpp_config() + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + rx = rx[0] + icmp = rx[ICMPv6DestUnreach] + + # 1 = "Communication with destination administratively prohibited" + self.assertEqual(icmp.code, 1) + + +class TestIPDisabled(VppTestCase): + """ IPv6 disabled """ + + def setUp(self): + super(TestIPDisabled, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(2)) + + # PG0 is IP enalbed + self.pg0.admin_up() + self.pg0.config_ip6() + self.pg0.resolve_ndp() + + # PG 1 is not IP enabled + self.pg1.admin_up() + + def tearDown(self): + super(TestIPDisabled, self).tearDown() + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) + + def test_ip_disabled(self): + """ IP Disabled """ + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_ff_01 = VppIpMRoute( + self, + "::", + "ffef::1", 128, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + is_ip6=1) + route_ff_01.add_vpp_config() + + pu = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IPv6(src="2001::1", dst=self.pg0.remote_ip6) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + pm = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + IPv6(src="2001::1", dst="ffef::1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, pu, "IPv6 disabled") + self.send_and_assert_no_replies(self.pg1, pm, "IPv6 disabled") + + # + # IP enable PG1 + # + self.pg1.config_ip6() + + # + # Now we get packets through + # + self.pg1.add_stream(pu) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + + self.pg1.add_stream(pm) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + + # + # Disable PG1 + # + self.pg1.unconfig_ip6() + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, pu, "IPv6 disabled") + self.send_and_assert_no_replies(self.pg1, pm, "IPv6 disabled") + + +class TestIP6LoadBalance(VppTestCase): + """ IPv6 Load-Balancing """ + + def setUp(self): + super(TestIP6LoadBalance, self).setUp() + + self.create_pg_interfaces(range(5)) + + mpls_tbl = VppMplsTable(self, 0) + mpls_tbl.add_vpp_config() + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip6() + i.resolve_ndp() + i.enable_mpls() + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip6() + i.admin_down() + i.disable_mpls() + super(TestIP6LoadBalance, self).tearDown() + + def send_and_expect_load_balancing(self, input, pkts, outputs): + input.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for oo in outputs: + rx = oo._get_capture(1) + self.assertNotEqual(0, len(rx)) + + def send_and_expect_one_itf(self, input, pkts, itf): + input.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = itf.get_capture(len(pkts)) + + def test_ip6_load_balance(self): + """ IPv6 Load-Balancing """ + + # + # An array of packets that differ only in the destination port + # - IP only + # - MPLS EOS + # - MPLS non-EOS + # - MPLS non-EOS with an entropy label + # + port_ip_pkts = [] + port_mpls_pkts = [] + port_mpls_neos_pkts = [] + port_ent_pkts = [] + + # + # An array of packets that differ only in the source address + # + src_ip_pkts = [] + src_mpls_pkts = [] + + for ii in range(65): + port_ip_hdr = (IPv6(dst="3000::1", src="3000:1::1") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100)) + port_ip_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + port_ip_hdr)) + port_mpls_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=66, ttl=2) / + port_ip_hdr)) + port_mpls_neos_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=67, ttl=2) / + MPLS(label=77, ttl=2) / + port_ip_hdr)) + port_ent_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=67, ttl=2) / + MPLS(label=14, ttl=2) / + MPLS(label=999, ttl=2) / + port_ip_hdr)) + src_ip_hdr = (IPv6(dst="3000::1", src="3000:1::%d" % ii) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + src_ip_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + src_ip_hdr)) + src_mpls_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + MPLS(label=66, ttl=2) / + src_ip_hdr)) + + # + # A route for the IP pacekts + # + route_3000_1 = VppIpRoute(self, "3000::1", 128, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_3000_1.add_vpp_config() + + # + # a local-label for the EOS packets + # + binding = VppMplsIpBind(self, 66, "3000::1", 128, is_ip6=1) + binding.add_vpp_config() + + # + # An MPLS route for the non-EOS packets + # + route_67 = VppMplsRoute(self, 67, 0, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + labels=[67], + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + labels=[67], + proto=DpoProto.DPO_PROTO_IP6)]) + route_67.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across the 2 paths + # - since the default hash config is to use IP src,dst and port + # src,dst + # We are not going to ensure equal amounts of packets across each link, + # since the hash algorithm is statistical and therefore this can never + # be guaranteed. But wuth 64 different packets we do expect some + # balancing. So instead just ensure there is traffic on each link. + # + self.send_and_expect_load_balancing(self.pg0, port_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, port_mpls_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_mpls_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, port_mpls_neos_pkts, + [self.pg1, self.pg2]) + + # + # The packets with Entropy label in should not load-balance, + # since the Entorpy value is fixed. + # + self.send_and_expect_one_itf(self.pg0, port_ent_pkts, self.pg1) + + # + # change the flow hash config so it's only IP src,dst + # - now only the stream with differing source address will + # load-balance + # + self.vapi.set_ip_flow_hash(0, is_ip6=1, src=1, dst=1, sport=0, dport=0) + + self.send_and_expect_load_balancing(self.pg0, src_ip_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_load_balancing(self.pg0, src_mpls_pkts, + [self.pg1, self.pg2]) + self.send_and_expect_one_itf(self.pg0, port_ip_pkts, self.pg2) + + # + # change the flow hash config back to defaults + # + self.vapi.set_ip_flow_hash(0, is_ip6=1, src=1, dst=1, sport=1, dport=1) + + # + # Recursive prefixes + # - testing that 2 stages of load-balancing occurs and there is no + # polarisation (i.e. only 2 of 4 paths are used) + # + port_pkts = [] + src_pkts = [] + + for ii in range(257): + port_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IPv6(dst="4000::1", src="4000:1::1") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100))) + src_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IPv6(dst="4000::1", src="4000:1::%d" % ii) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100))) + + route_3000_2 = VppIpRoute(self, "3000::2", 128, + [VppRoutePath(self.pg3.remote_ip6, + self.pg3.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg4.remote_ip6, + self.pg4.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_3000_2.add_vpp_config() + + route_4000_1 = VppIpRoute(self, "4000::1", 128, + [VppRoutePath("3000::1", + 0xffffffff, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath("3000::2", + 0xffffffff, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_4000_1.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across all 4 paths + # + self.vapi.cli("clear trace") + self.send_and_expect_load_balancing(self.pg0, port_pkts, + [self.pg1, self.pg2, + self.pg3, self.pg4]) + self.send_and_expect_load_balancing(self.pg0, src_pkts, + [self.pg1, self.pg2, + self.pg3, self.pg4]) + + # + # Recursive prefixes + # - testing that 2 stages of load-balancing no choices + # + port_pkts = [] + + for ii in range(257): + port_pkts.append((Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IPv6(dst="6000::1", src="6000:1::1") / + UDP(sport=1234, dport=1234 + ii) / + Raw('\xa5' * 100))) + + route_5000_2 = VppIpRoute(self, "5000::2", 128, + [VppRoutePath(self.pg3.remote_ip6, + self.pg3.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_5000_2.add_vpp_config() + + route_6000_1 = VppIpRoute(self, "6000::1", 128, + [VppRoutePath("5000::2", + 0xffffffff, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_6000_1.add_vpp_config() + + # + # inject the packet on pg0 - expect load-balancing across all 4 paths + # + self.vapi.cli("clear trace") + self.send_and_expect_one_itf(self.pg0, port_pkts, self.pg3) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip6_vrf_multi_instance.py b/test/test_ip6_vrf_multi_instance.py new file mode 100644 index 00000000..8dd228ae --- /dev/null +++ b/test/test_ip6_vrf_multi_instance.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python +"""IP6 VRF Multi-instance Test Case HLD: + +**NOTES:** + - higher number of pg-ip6 interfaces causes problems => only 15 pg-ip6 \ + interfaces in 5 VRFs are tested + - jumbo packets in configuration with 15 pg-ip6 interfaces leads to \ + problems too + +**config 1** + - add 15 pg-ip6 interfaces + - configure 5 hosts per pg-ip6 interface + - configure 4 VRFs + - add 3 pg-ip6 interfaces per VRF + +**test 1** + - send IP6 packets between all pg-ip6 interfaces in all VRF groups + +**verify 1** + - check VRF data by parsing output of ip6_fib_dump API command + - all packets received correctly in case of pg-ip6 interfaces in VRF + - no packet received in case of pg-ip6 interfaces not in VRF + +**config 2** + - reset 2 VRFs + +**test 2** + - send IP6 packets between all pg-ip6 interfaces in all VRF groups + +**verify 2** + - check VRF data by parsing output of ip6_fib_dump API command + - all packets received correctly in case of pg-ip6 interfaces in VRF + - no packet received in case of pg-ip6 interfaces not in VRF + +**config 3** + - add 1 of reset VRFs and 1 new VRF + +**test 3** + - send IP6 packets between all pg-ip6 interfaces in all VRF groups + +**verify 3** + - check VRF data by parsing output of ip6_fib_dump API command + - all packets received correctly in case of pg-ip6 interfaces in VRF + - no packet received in case of pg-ip6 interfaces not in VRF + +**config 4** + - reset all VRFs (i.e. no VRF except VRF=0 created) + +**test 4** + - send IP6 packets between all pg-ip6 interfaces in all VRF groups + +**verify 4** + - check VRF data by parsing output of ip6_fib_dump API command + - all packets received correctly in case of pg-ip6 interfaces in VRF + - no packet received in case of pg-ip6 interfaces not in VRF +""" + +import unittest +import random +import socket + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet6 import UDP, IPv6, ICMPv6ND_NS, ICMPv6ND_RA, \ + RouterAlert, IPv6ExtHdrHopByHop +from scapy.utils6 import in6_ismaddr, in6_isllsnmaddr, in6_getAddrType +from scapy.pton_ntop import inet_ntop +from scapy.data import IPV6_ADDR_UNICAST + +from framework import VppTestCase, VppTestRunner +from util import ppp + +# VRF status constants +VRF_NOT_CONFIGURED = 0 +VRF_CONFIGURED = 1 +VRF_RESET = 2 + + +def is_ipv6_misc_ext(p): + """ Is packet one of uninteresting IPv6 broadcasts (extended to filter out + ICMPv6 Neighbor Discovery - Neighbor Advertisement packets too)? """ + if p.haslayer(ICMPv6ND_RA): + if in6_ismaddr(p[IPv6].dst): + return True + if p.haslayer(ICMPv6ND_NS): + if in6_isllsnmaddr(p[IPv6].dst): + return True + if p.haslayer(IPv6ExtHdrHopByHop): + for o in p[IPv6ExtHdrHopByHop].options: + if isinstance(o, RouterAlert): + return True + return False + + +class TestIP6VrfMultiInst(VppTestCase): + """ IP6 VRF Multi-instance Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestIP6VrfMultiInst, cls).setUpClass() + + # Test variables + cls.hosts_per_pg = 5 + cls.nr_of_vrfs = 5 + cls.pg_ifs_per_vrf = 3 + + try: + # Create pg interfaces + cls.create_pg_interfaces( + range(cls.nr_of_vrfs * cls.pg_ifs_per_vrf)) + + # Packet flows mapping pg0 -> pg1, pg2 etc. + cls.flows = dict() + for i in range(len(cls.pg_interfaces)): + multiplicand = i / cls.pg_ifs_per_vrf + pg_list = [ + cls.pg_interfaces[multiplicand * cls.pg_ifs_per_vrf + j] + for j in range(cls.pg_ifs_per_vrf) + if (multiplicand * cls.pg_ifs_per_vrf + j) != i] + cls.flows[cls.pg_interfaces[i]] = pg_list + + # Packet sizes - jumbo packet (9018 bytes) skipped + cls.pg_if_packet_sizes = [64, 512, 1518] + + # Set up all interfaces + for pg_if in cls.pg_interfaces: + pg_if.admin_up() + pg_if.generate_remote_hosts(cls.hosts_per_pg) + + # Create list of VRFs + cls.vrf_list = list() + + # Create list of reset VRFs + cls.vrf_reset_list = list() + + # Create list of pg_interfaces in VRFs + cls.pg_in_vrf = list() + + # Create list of pg_interfaces not in BDs + cls.pg_not_in_vrf = [pg_if for pg_if in cls.pg_interfaces] + + # Create mapping of pg_interfaces to VRF IDs + cls.pg_if_by_vrf_id = dict() + for i in range(cls.nr_of_vrfs): + vrf_id = i + 1 + pg_list = [ + cls.pg_interfaces[i * cls.pg_ifs_per_vrf + j] + for j in range(cls.pg_ifs_per_vrf)] + cls.pg_if_by_vrf_id[vrf_id] = pg_list + + except Exception: + super(TestIP6VrfMultiInst, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + super(TestIP6VrfMultiInst, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestIP6VrfMultiInst, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show ip6 fib")) + self.logger.info(self.vapi.ppcli("show ip6 neighbors")) + + def create_vrf_and_assign_interfaces(self, count, start=1): + """ + Create required number of FIB tables / VRFs, put 3 l2-pg interfaces + to every FIB table / VRF. + + :param int count: Number of FIB tables / VRFs to be created. + :param int start: Starting number of the FIB table / VRF ID. \ + (Default value = 1) + """ + for i in range(count): + vrf_id = i + start + pg_if = self.pg_if_by_vrf_id[vrf_id][0] + dest_addr = pg_if.remote_hosts[0].ip6n + dest_addr_len = 64 + self.vapi.ip_table_add_del(vrf_id, is_add=1, is_ipv6=1) + self.vapi.ip_add_del_route( + dest_addr, dest_addr_len, pg_if.local_ip6n, is_ipv6=1, + table_id=vrf_id, is_multipath=1) + self.logger.info("IPv6 VRF ID %d created" % vrf_id) + if vrf_id not in self.vrf_list: + self.vrf_list.append(vrf_id) + if vrf_id in self.vrf_reset_list: + self.vrf_reset_list.remove(vrf_id) + for j in range(self.pg_ifs_per_vrf): + pg_if = self.pg_if_by_vrf_id[vrf_id][j] + pg_if.set_table_ip6(vrf_id) + self.logger.info("pg-interface %s added to IPv6 VRF ID %d" + % (pg_if.name, vrf_id)) + if pg_if not in self.pg_in_vrf: + self.pg_in_vrf.append(pg_if) + if pg_if in self.pg_not_in_vrf: + self.pg_not_in_vrf.remove(pg_if) + pg_if.config_ip6() + pg_if.disable_ipv6_ra() + pg_if.configure_ipv6_neighbors() + self.logger.debug(self.vapi.ppcli("show ip6 fib")) + self.logger.debug(self.vapi.ppcli("show ip6 neighbors")) + + def reset_vrf(self, vrf_id): + """ + Reset required FIB table / VRF. + + :param int vrf_id: The FIB table / VRF ID to be reset. + """ + # self.vapi.reset_vrf(vrf_id, is_ipv6=1) + self.vapi.reset_fib(vrf_id, is_ipv6=1) + if vrf_id in self.vrf_list: + self.vrf_list.remove(vrf_id) + if vrf_id not in self.vrf_reset_list: + self.vrf_reset_list.append(vrf_id) + for j in range(self.pg_ifs_per_vrf): + pg_if = self.pg_if_by_vrf_id[vrf_id][j] + pg_if.unconfig_ip6() + if pg_if in self.pg_in_vrf: + self.pg_in_vrf.remove(pg_if) + if pg_if not in self.pg_not_in_vrf: + self.pg_not_in_vrf.append(pg_if) + self.logger.info("IPv6 VRF ID %d reset" % vrf_id) + self.logger.debug(self.vapi.ppcli("show ip6 fib")) + self.logger.debug(self.vapi.ppcli("show ip6 neighbors")) + self.vapi.ip_table_add_del(vrf_id, is_add=0, is_ipv6=1) + + def create_stream(self, src_if, packet_sizes): + """ + Create input packet stream for defined interface using hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :return: Stream of packets. + """ + pkts = [] + src_hosts = src_if.remote_hosts + for dst_if in self.flows[src_if]: + for dst_host in dst_if.remote_hosts: + src_host = random.choice(src_hosts) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=src_if.local_mac, src=src_host.mac) / + IPv6(src=src_host.ip6, dst=dst_host.ip6) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + self.logger.debug("Input stream created for port %s. Length: %u pkt(s)" + % (src_if.name, len(pkts))) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + try: + ip = packet[IPv6] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertIsNotNone(next_info) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IPv6].src) + self.assertEqual(ip.dst, saved_packet[IPv6].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertIsNone( + remaining_packet, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def verify_vrf(self, vrf_id): + """ + Check if the FIB table / VRF ID is configured. + + :param int vrf_id: The FIB table / VRF ID to be verified. + :return: 1 if the FIB table / VRF ID is configured, otherwise return 0. + """ + ip6_fib_dump = self.vapi.ip6_fib_dump() + vrf_exist = False + vrf_count = 0 + for ip6_fib_details in ip6_fib_dump: + if ip6_fib_details.table_id == vrf_id: + if not vrf_exist: + vrf_exist = True + addr = inet_ntop(socket.AF_INET6, ip6_fib_details.address) + addrtype = in6_getAddrType(addr) + vrf_count += 1 if addrtype == IPV6_ADDR_UNICAST else 0 + if not vrf_exist and vrf_count == 0: + self.logger.info("IPv6 VRF ID %d is not configured" % vrf_id) + return VRF_NOT_CONFIGURED + elif vrf_exist and vrf_count == 0: + self.logger.info("IPv6 VRF ID %d has been reset" % vrf_id) + return VRF_RESET + else: + self.logger.info("IPv6 VRF ID %d is configured" % vrf_id) + return VRF_CONFIGURED + + def run_verify_test(self): + """ + Create packet streams for all configured l2-pg interfaces, send all \ + prepared packet streams and verify that: + - all packets received correctly on all pg-l2 interfaces assigned + to bridge domains + - no packet received on all pg-l2 interfaces not assigned to bridge + domains + + :raise RuntimeError: If no packet captured on l2-pg interface assigned + to the bridge domain or if any packet is captured on l2-pg + interface not assigned to the bridge domain. + """ + # Test + # Create incoming packet streams for packet-generator interfaces + for pg_if in self.pg_interfaces: + pkts = self.create_stream(pg_if, self.pg_if_packet_sizes) + pg_if.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for pg_if in self.pg_interfaces: + if pg_if in self.pg_in_vrf: + capture = pg_if.get_capture(remark="interface is in VRF") + self.verify_capture(pg_if, capture) + elif pg_if in self.pg_not_in_vrf: + pg_if.assert_nothing_captured(remark="interface is not in VRF", + filter_out_fn=is_ipv6_misc_ext) + self.logger.debug("No capture for interface %s" % pg_if.name) + else: + raise Exception("Unknown interface: %s" % pg_if.name) + + def test_ip6_vrf_01(self): + """ IP6 VRF Multi-instance test 1 - create 4 VRFs + """ + # Config 1 + # Create 4 VRFs + self.create_vrf_and_assign_interfaces(4) + + # Verify 1 + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_CONFIGURED) + + # Test 1 + self.run_verify_test() + + def test_ip6_vrf_02(self): + """ IP6 VRF Multi-instance test 2 - reset 2 VRFs + """ + # Config 2 + # Delete 2 VRFs + self.reset_vrf(1) + self.reset_vrf(2) + + # Verify 2 + for vrf_id in self.vrf_reset_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_RESET) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_CONFIGURED) + + # Test 2 + self.run_verify_test() + + # Reset routes learned from ICMPv6 Neighbor Discovery + for vrf_id in self.vrf_reset_list: + self.reset_vrf(vrf_id) + + def test_ip6_vrf_03(self): + """ IP6 VRF Multi-instance 3 - add 2 VRFs + """ + # Config 3 + # Add 1 of reset VRFs and 1 new VRF + self.create_vrf_and_assign_interfaces(1) + self.create_vrf_and_assign_interfaces(1, start=5) + + # Verify 3 + for vrf_id in self.vrf_reset_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_RESET) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_CONFIGURED) + + # Test 3 + self.run_verify_test() + + # Reset routes learned from ICMPv6 Neighbor Discovery + for vrf_id in self.vrf_reset_list: + self.reset_vrf(vrf_id) + + def test_ip6_vrf_04(self): + """ IP6 VRF Multi-instance test 4 - reset 4 VRFs + """ + # Config 4 + # Reset all VRFs (i.e. no VRF except VRF=0 created) + for i in range(len(self.vrf_list)): + self.reset_vrf(self.vrf_list[0]) + + # Verify 4 + for vrf_id in self.vrf_reset_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_RESET) + for vrf_id in self.vrf_list: + self.assertEqual(self.verify_vrf(vrf_id), VRF_CONFIGURED) + + # Test 4 + self.run_verify_test() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_ip_mcast.py b/test/test_ip_mcast.py new file mode 100644 index 00000000..7cad683c --- /dev/null +++ b/test/test_ip_mcast.py @@ -0,0 +1,760 @@ +#!/usr/bin/env python + +import unittest + +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppSubInterface, VppDot1QSubint, VppDot1ADSubint +from vpp_ip_route import VppIpMRoute, VppMRoutePath, VppMFibSignal, \ + MRouteItfFlags, MRouteEntryFlags, VppIpTable + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP, getmacbyip, ICMP +from scapy.layers.inet6 import IPv6, getmacbyip6 +from util import ppp + +# +# The number of packets sent is set to 90 so that when we replicate more than 3 +# times, which we do for some entries, we will generate more than 256 packets +# to the next node in the VLIB graph. Thus we are testing the code's +# correctness handling this over-flow +# +N_PKTS_IN_STREAM = 90 + + +class TestMFIB(VppTestCase): + """ MFIB Test Case """ + + def setUp(self): + super(TestMFIB, self).setUp() + + def test_mfib(self): + """ MFIB Unit Tests """ + error = self.vapi.cli("test mfib") + + if error: + self.logger.critical(error) + self.assertEqual(error.find("Failed"), -1) + + +class TestIPMcast(VppTestCase): + """ IP Multicast Test Case """ + + def setUp(self): + super(TestIPMcast, self).setUp() + + # create 8 pg interfaces + self.create_pg_interfaces(range(9)) + + # setup interfaces + for i in self.pg_interfaces[:8]: + i.admin_up() + i.config_ip4() + i.config_ip6() + i.resolve_arp() + i.resolve_ndp() + + # one more in a vrf + tbl4 = VppIpTable(self, 10) + tbl4.add_vpp_config() + self.pg8.set_table_ip4(10) + self.pg8.config_ip4() + + tbl6 = VppIpTable(self, 10, is_ip6=1) + tbl6.add_vpp_config() + self.pg8.set_table_ip6(10) + self.pg8.config_ip6() + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.admin_down() + + self.pg8.set_table_ip4(0) + self.pg8.set_table_ip6(0) + super(TestIPMcast, self).tearDown() + + def create_stream_ip4(self, src_if, src_ip, dst_ip, payload_size=0): + pkts = [] + # default to small packet sizes + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234)) + if not payload_size: + payload_size = 64 - len(p) + p = p / Raw('\xa5' * payload_size) + + for i in range(0, N_PKTS_IN_STREAM): + pkts.append(p) + return pkts + + def create_stream_ip6(self, src_if, src_ip, dst_ip): + pkts = [] + for i in range(0, N_PKTS_IN_STREAM): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IPv6(src=src_ip, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def verify_filter(self, capture, sent): + if not len(capture) == len(sent): + # filter out any IPv6 RAs from the captur + for p in capture: + if (p.haslayer(IPv6)): + capture.remove(p) + return capture + + def verify_capture_ip4(self, rx_if, sent): + rxd = rx_if.get_capture(len(sent)) + + try: + capture = self.verify_filter(rxd, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + + eth = rx[Ether] + self.assertEqual(eth.type, 0x800) + + tx_ip = tx[IP] + rx_ip = rx[IP] + + # check the MAC address on the RX'd packet is correctly formed + self.assertEqual(eth.dst, getmacbyip(rx_ip.dst)) + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + raise + + def verify_capture_ip6(self, rx_if, sent): + capture = rx_if.get_capture(len(sent)) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + + eth = rx[Ether] + self.assertEqual(eth.type, 0x86DD) + + tx_ip = tx[IPv6] + rx_ip = rx[IPv6] + + # check the MAC address on the RX'd packet is correctly formed + self.assertEqual(eth.dst, getmacbyip6(rx_ip.dst)) + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.hlim + 1, tx_ip.hlim) + + def test_ip_mcast(self): + """ IP Multicast Replication """ + + # + # a stream that matches the default route. gets dropped. + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "1.1.1.1", "232.1.1.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on default route") + + # + # A (*,G). + # one accepting interface, pg0, 7 forwarding interfaces + # many forwarding interfaces test the case where the replicare DPO + # needs to use extra cache lines for the buckets. + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg3.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg4.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg5.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg6.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg7.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_232_1_1_1.add_vpp_config() + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_1_1_1_1_232_1_1_1 = VppIpMRoute( + self, + "1.1.1.1", + "232.1.1.1", 64, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_1_1_1_1_232_1_1_1.add_vpp_config() + + # + # An (*,G/m). + # one accepting interface, pg0, 1 forwarding interfaces + # + route_232 = VppIpMRoute( + self, + "0.0.0.0", + "232.0.0.0", 8, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_232.add_vpp_config() + + # + # a stream that matches the route for (1.1.1.1,232.1.1.1) + # small packets + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "1.1.1.1", "232.1.1.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1->7 + self.verify_capture_ip4(self.pg1, tx) + self.verify_capture_ip4(self.pg2, tx) + + # no replications on Pg0 + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on PG0") + self.pg3.assert_nothing_captured( + remark="IP multicast packets forwarded on PG3") + + # + # a stream that matches the route for (1.1.1.1,232.1.1.1) + # large packets + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "1.1.1.1", "232.1.1.1", + payload_size=1024) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1->7 + self.verify_capture_ip4(self.pg1, tx) + self.verify_capture_ip4(self.pg2, tx) + + # no replications on Pg0 + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on PG0") + self.pg3.assert_nothing_captured( + remark="IP multicast packets forwarded on PG3") + + # + # a stream that matches the route for (*,232.0.0.0/8) + # Send packets with the 9th bit set so we test the correct clearing + # of that bit in the mac rewrite + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "1.1.1.1", "232.255.255.255") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1 only + self.verify_capture_ip4(self.pg1, tx) + + # no replications on Pg0, Pg2 not Pg3 + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on PG0") + self.pg2.assert_nothing_captured( + remark="IP multicast packets forwarded on PG2") + self.pg3.assert_nothing_captured( + remark="IP multicast packets forwarded on PG3") + + # + # a stream that matches the route for (*,232.1.1.1) + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "1.1.1.2", "232.1.1.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1, 2, 3. + self.verify_capture_ip4(self.pg1, tx) + self.verify_capture_ip4(self.pg2, tx) + self.verify_capture_ip4(self.pg3, tx) + self.verify_capture_ip4(self.pg4, tx) + self.verify_capture_ip4(self.pg5, tx) + self.verify_capture_ip4(self.pg6, tx) + self.verify_capture_ip4(self.pg7, tx) + + route_232_1_1_1.remove_vpp_config() + route_1_1_1_1_232_1_1_1.remove_vpp_config() + route_232.remove_vpp_config() + + def test_ip6_mcast(self): + """ IPv6 Multicast Replication """ + + # + # a stream that matches the default route. gets dropped. + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg0, "2001::1", "ff01::1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured( + remark="IPv6 multicast packets forwarded on default route") + + # + # A (*,G). + # one accepting interface, pg0, 3 forwarding interfaces + # + route_ff01_1 = VppIpMRoute( + self, + "::", + "ff01::1", 128, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg3.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + is_ip6=1) + route_ff01_1.add_vpp_config() + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_2001_ff01_1 = VppIpMRoute( + self, + "2001::1", + "ff01::1", 256, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + is_ip6=1) + route_2001_ff01_1.add_vpp_config() + + # + # An (*,G/m). + # one accepting interface, pg0, 1 forwarding interface + # + route_ff01 = VppIpMRoute( + self, + "::", + "ff01::", 16, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + is_ip6=1) + route_ff01.add_vpp_config() + + # + # a stream that matches the route for (*, ff01::/16) + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg0, "2002::1", "ff01:2::255") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1 + self.verify_capture_ip6(self.pg1, tx) + + # no replications on Pg0, Pg3 + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on PG0") + self.pg2.assert_nothing_captured( + remark="IP multicast packets forwarded on PG2") + self.pg3.assert_nothing_captured( + remark="IP multicast packets forwarded on PG3") + + # + # Bounce the interface and it should still work + # + self.pg1.admin_down() + self.pg0.add_stream(tx) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg1.assert_nothing_captured( + remark="IP multicast packets forwarded on down PG1") + + self.pg1.admin_up() + self.pg0.add_stream(tx) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.verify_capture_ip6(self.pg1, tx) + + # + # a stream that matches the route for (*,ff01::1) + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg0, "2002::2", "ff01::1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1, 2, 3. + self.verify_capture_ip6(self.pg1, tx) + self.verify_capture_ip6(self.pg2, tx) + self.verify_capture_ip6(self.pg3, tx) + + # no replications on Pg0 + self.pg0.assert_nothing_captured( + remark="IPv6 multicast packets forwarded on PG0") + + # + # a stream that matches the route for (2001::1, ff00::1) + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg0, "2001::1", "ff01::1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1, 2, + self.verify_capture_ip6(self.pg1, tx) + self.verify_capture_ip6(self.pg2, tx) + + # no replications on Pg0, Pg3 + self.pg0.assert_nothing_captured( + remark="IP multicast packets forwarded on PG0") + self.pg3.assert_nothing_captured( + remark="IP multicast packets forwarded on PG3") + + route_ff01.remove_vpp_config() + route_ff01_1.remove_vpp_config() + route_2001_ff01_1.remove_vpp_config() + + def _mcast_connected_send_stream(self, dst_ip): + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, + self.pg0.remote_ip4, + dst_ip) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1. + self.verify_capture_ip4(self.pg1, tx) + + return tx + + def test_ip_mcast_connected(self): + """ IP Multicast Connected Source check """ + + # + # A (*,G). + # one accepting interface, pg0, 1 forwarding interfaces + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + + route_232_1_1_1.add_vpp_config() + route_232_1_1_1.update_entry_flags( + MRouteEntryFlags.MFIB_ENTRY_FLAG_CONNECTED) + + # + # Now the (*,G) is present, send from connected source + # + tx = self._mcast_connected_send_stream("232.1.1.1") + + # + # Constrct a representation of the signal we expect on pg0 + # + signal_232_1_1_1_itf_0 = VppMFibSignal(self, + route_232_1_1_1, + self.pg0.sw_if_index, + tx[0]) + + # + # read the only expected signal + # + signals = self.vapi.mfib_signal_dump() + + self.assertEqual(1, len(signals)) + + signal_232_1_1_1_itf_0.compare(signals[0]) + + # + # reading the signal allows for the generation of another + # so send more packets and expect the next signal + # + tx = self._mcast_connected_send_stream("232.1.1.1") + + signals = self.vapi.mfib_signal_dump() + self.assertEqual(1, len(signals)) + signal_232_1_1_1_itf_0.compare(signals[0]) + + # + # A Second entry with connected check + # one accepting interface, pg0, 1 forwarding interfaces + # + route_232_1_1_2 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.2", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + + route_232_1_1_2.add_vpp_config() + route_232_1_1_2.update_entry_flags( + MRouteEntryFlags.MFIB_ENTRY_FLAG_CONNECTED) + + # + # Send traffic to both entries. One read should net us two signals + # + signal_232_1_1_2_itf_0 = VppMFibSignal(self, + route_232_1_1_2, + self.pg0.sw_if_index, + tx[0]) + tx = self._mcast_connected_send_stream("232.1.1.1") + tx2 = self._mcast_connected_send_stream("232.1.1.2") + + # + # read the only expected signal + # + signals = self.vapi.mfib_signal_dump() + + self.assertEqual(2, len(signals)) + + signal_232_1_1_1_itf_0.compare(signals[1]) + signal_232_1_1_2_itf_0.compare(signals[0]) + + route_232_1_1_1.remove_vpp_config() + route_232_1_1_2.remove_vpp_config() + + def test_ip_mcast_signal(self): + """ IP Multicast Signal """ + + # + # A (*,G). + # one accepting interface, pg0, 1 forwarding interfaces + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + + route_232_1_1_1.add_vpp_config() + route_232_1_1_1.update_entry_flags( + MRouteEntryFlags.MFIB_ENTRY_FLAG_SIGNAL) + + # + # Now the (*,G) is present, send from connected source + # + tx = self._mcast_connected_send_stream("232.1.1.1") + + # + # Constrct a representation of the signal we expect on pg0 + # + signal_232_1_1_1_itf_0 = VppMFibSignal(self, + route_232_1_1_1, + self.pg0.sw_if_index, + tx[0]) + + # + # read the only expected signal + # + signals = self.vapi.mfib_signal_dump() + + self.assertEqual(1, len(signals)) + + signal_232_1_1_1_itf_0.compare(signals[0]) + + # + # reading the signal allows for the generation of another + # so send more packets and expect the next signal + # + tx = self._mcast_connected_send_stream("232.1.1.1") + + signals = self.vapi.mfib_signal_dump() + self.assertEqual(1, len(signals)) + signal_232_1_1_1_itf_0.compare(signals[0]) + + # + # Set the negate-signal on the accepting interval - the signals + # should stop + # + route_232_1_1_1.update_path_flags( + self.pg0.sw_if_index, + (MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT | + MRouteItfFlags.MFIB_ITF_FLAG_NEGATE_SIGNAL)) + + self.vapi.cli("clear trace") + tx = self._mcast_connected_send_stream("232.1.1.1") + + signals = self.vapi.mfib_signal_dump() + self.assertEqual(0, len(signals)) + + # + # Clear the SIGNAL flag on the entry and the signals should + # come back since the interface is still NEGATE-SIGNAL + # + route_232_1_1_1.update_entry_flags( + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE) + + tx = self._mcast_connected_send_stream("232.1.1.1") + + signals = self.vapi.mfib_signal_dump() + self.assertEqual(1, len(signals)) + signal_232_1_1_1_itf_0.compare(signals[0]) + + # + # Lastly remove the NEGATE-SIGNAL from the interface and the + # signals should stop + # + route_232_1_1_1.update_path_flags(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT) + + tx = self._mcast_connected_send_stream("232.1.1.1") + signals = self.vapi.mfib_signal_dump() + self.assertEqual(0, len(signals)) + + # + # Cleanup + # + route_232_1_1_1.remove_vpp_config() + + def test_ip_mcast_vrf(self): + """ IP Multicast Replication in non-default table""" + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_1_1_1_1_232_1_1_1 = VppIpMRoute( + self, + "1.1.1.1", + "232.1.1.1", 64, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg8.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + table_id=10) + route_1_1_1_1_232_1_1_1.add_vpp_config() + + # + # a stream that matches the route for (1.1.1.1,232.1.1.1) + # small packets + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg8, "1.1.1.1", "232.1.1.1") + self.pg8.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1 & 2 + self.verify_capture_ip4(self.pg1, tx) + self.verify_capture_ip4(self.pg2, tx) + + def test_ip6_mcast_vrf(self): + """ IPv6 Multicast Replication in non-default table""" + + # + # An (S,G). + # one accepting interface, pg0, 2 forwarding interfaces + # + route_2001_ff01_1 = VppIpMRoute( + self, + "2001::1", + "ff01::1", 256, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg8.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD), + VppMRoutePath(self.pg2.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + table_id=10, + is_ip6=1) + route_2001_ff01_1.add_vpp_config() + + # + # a stream that matches the route for (2001::1, ff00::1) + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip6(self.pg8, "2001::1", "ff01::1") + self.pg8.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # We expect replications on Pg1, 2, + self.verify_capture_ip6(self.pg1, tx) + self.verify_capture_ip6(self.pg2, tx) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_jvpp.py b/test/test_jvpp.py new file mode 100644 index 00000000..2497ff68 --- /dev/null +++ b/test/test_jvpp.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import os +import subprocess + +from framework import VppTestCase + +# Api files path +API_FILES_PATH = "vpp/vpp-api/java" + +# Registry jar file name prefix +REGISTRY_JAR_PREFIX = "jvpp-registry" + + +class TestJVpp(VppTestCase): + """ JVPP Core Test Case """ + + def invoke_for_jvpp_core(self, api_jar_name, test_class_name): + self.jvpp_connection_test(api_jar_name=api_jar_name, + test_class_name=test_class_name, + timeout=10) + + def test_vpp_core_callback_api(self): + """ JVPP Core Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-core", + test_class_name="io.fd.vpp.jvpp.core.test." + "CallbackApiTest") + + def test_vpp_core_future_api(self): + """JVPP Core Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-core", + test_class_name="io.fd.vpp.jvpp.core.test." + "FutureApiTest") + + def test_vpp_acl_callback_api(self): + """ JVPP Acl Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-acl", + test_class_name="io.fd.vpp.jvpp.acl.test." + "CallbackApiTest") + + def test_vpp_acl_future_api(self): + """JVPP Acl Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-acl", + test_class_name="io.fd.vpp.jvpp.acl.test." + "FutureApiTest") + + def test_vpp_ioamexport_callback_api(self): + """ JVPP Ioamexport Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioamexport", + test_class_name="io.fd.vpp.jvpp.ioamexport." + "test.CallbackApiTest") + + def test_vpp_ioamexport_future_api(self): + """JVPP Ioamexport Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioamexport", + test_class_name="io.fd.vpp.jvpp.ioamexport." + "test.FutureApiTest") + + def test_vpp_ioampot_callback_api(self): + """ JVPP Ioampot Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioampot", + test_class_name="io.fd.vpp.jvpp.ioampot." + "test.CallbackApiTest") + + def test_vpp_ioampot_future_api(self): + """JVPP Ioampot Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioampot", + test_class_name="io.fd.vpp.jvpp.ioampot." + "test.FutureApiTest") + + def test_vpp_ioamtrace_callback_api(self): + """ JVPP Ioamtrace Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioamtrace", + test_class_name="io.fd.vpp.jvpp.ioamtrace." + "test.CallbackApiTest") + + def test_vpp_ioamtrace_future_api(self): + """JVPP Ioamtrace Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-ioamtrace", + test_class_name="io.fd.vpp.jvpp.ioamtrace." + "test.FutureApiTest") + + def test_vpp_snat_callback_api(self): + """ JVPP Snat Callback Api Test Case """ + self.invoke_for_jvpp_core(api_jar_name="jvpp-nat", + test_class_name="io.fd.vpp.jvpp.nat.test." + "CallbackApiTest") + + def test_vpp_snat_future_api(self): + """JVPP Snat Future Api Test Case""" + self.invoke_for_jvpp_core(api_jar_name="jvpp-nat", + test_class_name="io.fd.vpp.jvpp.nat.test." + "FutureApiTest") + + def full_jar_name(self, install_dir, jar_name, version): + return os.path.join(install_dir, API_FILES_PATH, + "{0}-{1}.jar".format(jar_name, version)) + + def jvpp_connection_test(self, api_jar_name, test_class_name, timeout): + install_dir = os.getenv('VPP_TEST_BUILD_DIR') + self.logger.info("Install directory : {0}".format(install_dir)) + + version_reply = self.vapi.show_version() + version = version_reply.version.split("-")[0] + registry_jar_path = self.full_jar_name(install_dir, + REGISTRY_JAR_PREFIX, version) + self.logger.info("JVpp Registry jar path : {0}" + .format(registry_jar_path)) + + api_jar_path = self.full_jar_name(install_dir, api_jar_name, version) + self.logger.info("Api jar path : {0}".format(api_jar_path)) + + # passes shm prefix as parameter to create connection with same value + command = ["java", "-cp", + "{0}:{1}".format(registry_jar_path, api_jar_path), + test_class_name, "/{0}-vpe-api".format(self.shm_prefix)] + self.logger.info("Test Command : {0}, Timeout : {1}". + format(command, timeout)) + + self.process = subprocess.Popen(command, shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, bufsize=1, + universal_newlines=True) + + out, err = self.process.communicate() + self.logger.info("Process output : {0}{1}".format(os.linesep, out)) + self.logger.info("Process error output : {0}{1}" + .format(os.linesep, err)) + self.assert_equal(self.process.returncode, 0, "process return code") + + def tearDown(self): + self.logger.info("Tearing down jvpp test") + super(TestJVpp, self).tearDown() + if hasattr(self, 'process') and self.process.poll() is None: + self.process.kill() diff --git a/test/test_l2_fib.py b/test/test_l2_fib.py new file mode 100644 index 00000000..52bf9c86 --- /dev/null +++ b/test/test_l2_fib.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python +"""L2 FIB Test Case HLD: + +**config 1** + - add 4 pg-l2 interfaces + - configure them into l2bd + - configure 100 MAC entries in L2 fib - 25 MACs per interface + - L2 MAC learning and unknown unicast flooding disabled in l2bd + - configure 100 MAC entries in L2 fib - 25 MACs per interface + +**test 1** + - send L2 MAC frames between all 4 pg-l2 interfaces for all of 100 MAC \ + entries in the FIB + +**verify 1** + - all packets received correctly + +**config 2** + - delete 12 MAC entries - 3 MACs per interface + +**test 2a** + - send L2 MAC frames between all 4 pg-l2 interfaces for non-deleted MAC \ + entries + +**verify 2a** + - all packets received correctly + +**test 2b** + - send L2 MAC frames between all 4 pg-l2 interfaces for all of 12 deleted \ + MAC entries + +**verify 2b** + - no packet received on all 4 pg-l2 interfaces + +**config 3** + - configure new 100 MAC entries in L2 fib - 25 MACs per interface + +**test 3** + - send L2 MAC frames between all 4 pg-l2 interfaces for all of 188 MAC \ + entries in the FIB + +**verify 3** + - all packets received correctly + +**config 4** + - delete 160 MAC entries, 40 MACs per interface + +**test 4a** + - send L2 MAC frames between all 4 pg-l2 interfaces for all of 28 \ + non-deleted MAC entries + +**verify 4a** + - all packets received correctly + +**test 4b** + - try send L2 MAC frames between all 4 pg-l2 interfaces for all of 172 \ + deleted MAC entries + +**verify 4b** + - no packet received on all 4 pg-l2 interfaces +""" + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp + + +class TestL2fib(VppTestCase): + """ L2 FIB Test Case """ + + @classmethod + def bd_ifs(cls, bd_id): + return range((bd_id - 1) * cls.n_ifs_per_bd, bd_id * cls.n_ifs_per_bd) + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + + :var int bd_id: Bridge domain ID. + """ + super(TestL2fib, cls).setUpClass() + + try: + n_brs = cls.n_brs = range(1, 3) + cls.n_ifs_per_bd = 4 + n_ifs = range(cls.n_ifs_per_bd * len(cls.n_brs)) + # Create 4 pg interfaces + cls.create_pg_interfaces(n_ifs) + + cls.flows = dict() + for bd_id in n_brs: + # Packet flows mapping pg0 -> pg1, pg2, pg3 etc. + ifs = cls.bd_ifs(bd_id) + for j in ifs: + cls.flows[cls.pg_interfaces[j]] = [ + cls.pg_interfaces[x] for x in ifs if x != j] + + # Packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + for bd_id in n_brs: + # Create BD with MAC learning and unknown unicast flooding + # disabled and put interfaces to this BD + cls.vapi.bridge_domain_add_del( + bd_id=bd_id, uu_flood=0, learn=0) + ifs = [cls.pg_interfaces[i] for i in cls.bd_ifs(bd_id)] + for pg_if in ifs: + cls.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, + bd_id=bd_id) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + # Mapping between packet-generator index and lists of test hosts + cls.hosts = dict() + cls.learned_hosts = dict() + cls.fib_hosts = dict() + cls.deleted_hosts = dict() + for pg_if in cls.pg_interfaces: + swif = pg_if.sw_if_index + cls.hosts[swif] = [] + cls.learned_hosts[swif] = [] + cls.fib_hosts[swif] = [] + cls.deleted_hosts[swif] = [] + + except Exception: + super(TestL2fib, cls).tearDownClass() + raise + + def setUp(self): + super(TestL2fib, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2fib, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + for bd_id in self.n_brs: + self.logger.info(self.vapi.ppcli("show bridge-domain %s detail" + % bd_id)) + + def create_hosts(self, n_hosts_per_if, subnet): + """ + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address. + + :param int n_hosts_per_if: Number of per interface hosts to + create MAC/IPv4 addresses for. + """ + + for pg_if in self.pg_interfaces: + swif = pg_if.sw_if_index + + def mac(j): return "00:00:%02x:ff:%02x:%02x" % (subnet, swif, j) + + def ip(j): return "172.%02u.1%02x.%u" % (subnet, swif, j) + + def h(j): return Host(mac(j), ip(j)) + self.hosts[swif] = [h(j) for j in range(n_hosts_per_if)] + + def learn_hosts(self, bd_id, n_hosts_per_if): + """ + Create L2 MAC packet stream with host MAC addresses per interface to + let the bridge domain learn these MAC addresses. + + :param int bd_id: BD to teach + :param int n_hosts_per_if: number of hosts + """ + self.vapi.bridge_flags(bd_id, 1, 1) + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for pg_if in ifs: + swif = pg_if.sw_if_index + hosts = self.hosts[swif] + lhosts = self.learned_hosts[swif] + packets = [] + for j in range(n_hosts_per_if): + host = hosts.pop() + lhosts.append(host) + packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac)) + packets.append(packet) + pg_if.add_stream(packets) + self.logger.info("Sending broadcast eth frames for MAC learning") + self.pg_start() + + def config_l2_fib_entries(self, bd_id, n_hosts_per_if): + """ + Config required number of L2 FIB entries. + + :param int bd_id: BD's id + :param int count: Number of L2 FIB entries to be created. + :param int start: Starting index of the host list. (Default value = 0) + """ + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for pg_if in ifs: + swif = pg_if.sw_if_index + hosts = self.hosts[swif] + fhosts = self.fib_hosts[swif] + for j in range(n_hosts_per_if): + host = hosts.pop() + self.vapi.l2fib_add_del( + host.mac, bd_id, swif, static_mac=1) + fhosts.append(host) + # del hosts[0] + self.logger.info("Configure %d L2 FIB entries .." % + len(self.pg_interfaces) * n_hosts_per_if) + self.logger.info(self.vapi.ppcli("show l2fib")) + + def delete_l2_fib_entry(self, bd_id, n_hosts_per_if): + """ + Delete required number of L2 FIB entries. + + :param int count: Number of L2 FIB entries to be created. + """ + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for pg_if in ifs: + swif = pg_if.sw_if_index + hosts = self.fib_hosts[swif] + dhosts = self.deleted_hosts[swif] + for j in range(n_hosts_per_if): + host = hosts.pop() + self.vapi.l2fib_add_del( + host.mac, bd_id, swif, is_add=0) + dhosts.append(host) + self.logger.info(self.vapi.ppcli("show l2fib")) + + def flush_int(self, swif): + """ + Flush swif L2 FIB entries. + + :param int swif: sw if index. + """ + flushed = dict() + self.vapi.l2fib_flush_int(swif) + self.deleted_hosts[swif] = self.learned_hosts[swif] + \ + self.deleted_hosts[swif] + flushed[swif] = self.learned_hosts[swif] + self.learned_hosts[swif] = [] + self.logger.info(self.vapi.ppcli("show l2fib")) + return flushed + + def flush_bd(self, bd_id): + """ + Flush bd_id L2 FIB entries. + + :param int bd_id: Bridge Domain id. + """ + self.vapi.l2fib_flush_bd(bd_id) + flushed = dict() + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for pg_if in ifs: + swif = pg_if.sw_if_index + self.deleted_hosts[swif] = self.learned_hosts[swif] + \ + self.deleted_hosts[swif] + flushed[swif] = self.learned_hosts[swif] + self.learned_hosts[swif] = [] + self.logger.info(self.vapi.ppcli("show l2fib")) + return flushed + + def flush_all(self): + """ + Flush All L2 FIB entries. + """ + self.vapi.l2fib_flush_all() + flushed = dict() + for pg_if in self.pg_interfaces: + swif = pg_if.sw_if_index + self.deleted_hosts[swif] = self.learned_hosts[swif] + \ + self.deleted_hosts[swif] + flushed[swif] = self.learned_hosts[swif] + self.learned_hosts[swif] = [] + self.logger.info(self.vapi.ppcli("show l2fib")) + return flushed + + def create_stream(self, src_if, packet_sizes, if_src_hosts=None, + if_dst_hosts=None): + """ + Create input packet stream for defined interface using hosts or + deleted_hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :param boolean deleted: Set to True if deleted_hosts list required. + :return: Stream of packets. + """ + if not if_src_hosts: + if_src_hosts = self.fib_hosts + if not if_dst_hosts: + if_dst_hosts = self.fib_hosts + src_hosts = if_src_hosts[src_if.sw_if_index] + if not src_hosts: + return [] + pkts = [] + for dst_if in self.flows[src_if]: + dst_swif = dst_if.sw_if_index + if dst_swif not in if_dst_hosts: + continue + dst_hosts = if_dst_hosts[dst_swif] + for i in range(0, len(dst_hosts)): + dst_host = dst_hosts[i] + src_host = random.choice(src_hosts) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + payload_info = self.payload_to_info(str(packet[Raw])) + try: + ip = packet[IP] + udp = packet[UDP] + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def run_verify_test(self, bd_id, dst_hosts=None): + # Test + # Create incoming packet streams for packet-generator interfaces + if not dst_hosts: + dst_hosts = self.fib_hosts + self.reset_packet_infos() + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for i in ifs: + pkts = self.create_stream( + i, self.pg_if_packet_sizes, if_dst_hosts=dst_hosts) + i.add_stream(pkts) + + self.vapi.bridge_flags(bd_id, 0, 1) + # Enable packet capture and start packet sending + self.pg_enable_capture(ifs) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for i in ifs: + if not dst_hosts[i.sw_if_index]: + continue + capture = i.get_capture() + self.logger.info("Verifying capture on interface %s" % i.name) + self.verify_capture(i, capture) + + def run_verify_negat_test(self, bd_id, dst_hosts=None): + # Test + # Create incoming packet streams for packet-generator interfaces for + # deleted MAC addresses + if not dst_hosts: + dst_hosts = self.deleted_hosts + self.reset_packet_infos() + ifs = [self.pg_interfaces[i] for i in self.bd_ifs(bd_id)] + for i in ifs: + pkts = self.create_stream( + i, self.pg_if_packet_sizes, if_dst_hosts=dst_hosts) + if pkts: + i.add_stream(pkts) + + self.vapi.bridge_flags(bd_id, 0, 1) + # Enable packet capture and start packet sending + self.pg_enable_capture(ifs) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + timeout = 1 + for i in ifs: + i.get_capture(0, timeout=timeout) + i.assert_nothing_captured(remark="outgoing interface") + timeout = 0.1 + + def test_l2_fib_01(self): + """ L2 FIB test 1 - program 100 MAC addresses + """ + # Config 1 + # Create test host entries + self.create_hosts(100, subnet=17) + + # Add first 100 MAC entries to L2 FIB + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=100) + + # Test 1 + self.run_verify_test(bd_id=1) + + def test_l2_fib_02(self): + """ L2 FIB test 2 - delete 12 MAC entries + """ + # Config 2 + # Delete 12 MAC entries per interface from L2 FIB + self.delete_l2_fib_entry(bd_id=1, n_hosts_per_if=12) + + # Test 2a + self.run_verify_test(bd_id=1) + + # Verify 2a + self.run_verify_negat_test(bd_id=1) + + def test_l2_fib_03(self): + """ L2 FIB test 3 - program new 100 MAC addresses + """ + # Config 3 + # Create new test host entries + self.create_hosts(100, subnet=22) + + # Add new 100 MAC entries to L2 FIB + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=100) + + # Test 3 + self.run_verify_test(bd_id=1) + + def test_l2_fib_04(self): + """ L2 FIB test 4 - delete 160 MAC entries + """ + # Config 4 + # Delete 160 MAC entries per interface from L2 FIB + self.delete_l2_fib_entry(bd_id=1, n_hosts_per_if=160) + + # Test 4a + self.run_verify_negat_test(bd_id=1) + + def test_l2_fib_05(self): + """ L2 FIB test 5 - Program 10 new MAC entries, learn 10 + """ + self.create_hosts(20, subnet=35) + + self.learn_hosts(bd_id=1, n_hosts_per_if=10) + self.learn_hosts(bd_id=2, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=2, n_hosts_per_if=10) + self.run_verify_test(bd_id=1, dst_hosts=self.learned_hosts) + self.run_verify_test(bd_id=2, dst_hosts=self.learned_hosts) + + def test_l2_fib_06(self): + """ L2 FIB test 6 - flush first interface + """ + self.create_hosts(20, subnet=36) + + self.learn_hosts(bd_id=1, n_hosts_per_if=10) + self.learn_hosts(bd_id=2, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=2, n_hosts_per_if=10) + flushed = self.flush_int(self.pg_interfaces[0].sw_if_index) + self.run_verify_test(bd_id=1, dst_hosts=self.learned_hosts) + self.run_verify_negat_test(bd_id=1, dst_hosts=flushed) + + def test_l2_fib_07(self): + """ L2 FIB test 7 - flush bd_id + """ + self.create_hosts(20, subnet=37) + + self.learn_hosts(bd_id=1, n_hosts_per_if=10) + self.learn_hosts(bd_id=2, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=2, n_hosts_per_if=10) + flushed = self.flush_bd(bd_id=1) + self.run_verify_negat_test(bd_id=1, dst_hosts=flushed) + self.run_verify_test(bd_id=2, dst_hosts=self.learned_hosts) + + def test_l2_fib_08(self): + """ L2 FIB test 8 - flush all + """ + self.create_hosts(20, subnet=38) + + self.learn_hosts(bd_id=1, n_hosts_per_if=10) + self.learn_hosts(bd_id=2, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=1, n_hosts_per_if=10) + self.config_l2_fib_entries(bd_id=2, n_hosts_per_if=10) + flushed = self.flush_all() + self.run_verify_negat_test(bd_id=1, dst_hosts=flushed) + self.run_verify_negat_test(bd_id=2, dst_hosts=flushed) + + def test_l2_fib_09(self): + """ L2 FIB test 9 - mac learning events + """ + self.create_hosts(10, subnet=39) + + self.vapi.want_macs_learn_events() + self.learn_hosts(bd_id=1, n_hosts_per_if=10) + + self.sleep(1) + self.logger.info(self.vapi.ppcli("show l2fib")) + evs = self.vapi.collect_events() + learned_macs = { + e.mac[i].mac_addr for e in evs for i in range(e.n_macs)} + macs = {h.bin_mac for swif_hs in self.learned_hosts.itervalues() + for h in swif_hs} + self.vapi.want_macs_learn_events(enable_disable=0) + self.assertEqual(len(learned_macs ^ macs), 0) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2bd.py b/test/test_l2bd.py new file mode 100644 index 00000000..30708a46 --- /dev/null +++ b/test/test_l2bd.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp +from vpp_sub_interface import VppDot1QSubint, VppDot1ADSubint + + +class TestL2bd(VppTestCase): + """ L2BD Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + + :var int bd_id: Bridge domain ID. + :var int mac_entries_count: Number of MAC entries for bridge-domain to + learn. + :var int dot1q_tag: VLAN tag for dot1q sub-interface. + :var int dot1ad_sub_id: SubID of dot1ad sub-interface. + :var int dot1ad_outer_tag: VLAN S-tag for dot1ad sub-interface. + :var int dot1ad_inner_tag: VLAN C-tag for dot1ad sub-interface. + :var int sl_pkts_per_burst: Number of packets in burst for single-loop + test. + :var int dl_pkts_per_burst: Number of packets in burst for dual-loop + test. + """ + super(TestL2bd, cls).setUpClass() + + # Test variables + cls.bd_id = 1 + cls.mac_entries_count = 100 + # cls.dot1q_sub_id = 100 + cls.dot1q_tag = 100 + cls.dot1ad_sub_id = 20 + cls.dot1ad_outer_tag = 200 + cls.dot1ad_inner_tag = 300 + cls.sl_pkts_per_burst = 2 + cls.dl_pkts_per_burst = 257 + + try: + # create 3 pg interfaces + cls.create_pg_interfaces(range(3)) + + # create 2 sub-interfaces for pg1 and pg2 + cls.sub_interfaces = [ + VppDot1QSubint(cls, cls.pg1, cls.dot1q_tag), + VppDot1ADSubint(cls, cls.pg2, cls.dot1ad_sub_id, + cls.dot1ad_outer_tag, cls.dot1ad_inner_tag)] + + # packet flows mapping pg0 -> pg1, pg2, etc. + cls.flows = dict() + cls.flows[cls.pg0] = [cls.pg1, cls.pg2] + cls.flows[cls.pg1] = [cls.pg0, cls.pg2] + cls.flows[cls.pg2] = [cls.pg0, cls.pg1] + + # packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + cls.sub_if_packet_sizes = [64, 512, 1518 + 4, 9018 + 4] + + cls.interfaces = list(cls.pg_interfaces) + cls.interfaces.extend(cls.sub_interfaces) + + # Create BD with MAC learning enabled and put interfaces and + # sub-interfaces to this BD + for pg_if in cls.pg_interfaces: + sw_if_index = pg_if.sub_if.sw_if_index \ + if hasattr(pg_if, 'sub_if') else pg_if.sw_if_index + cls.vapi.sw_interface_set_l2_bridge(sw_if_index, + bd_id=cls.bd_id) + + # setup all interfaces + for i in cls.interfaces: + i.admin_up() + + # mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + + # create test host entries and inject packets to learn MAC entries + # in the bridge-domain + cls.create_hosts_and_learn(cls.mac_entries_count) + cls.logger.info(cls.vapi.ppcli("show l2fib")) + + except Exception: + super(TestL2bd, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + super(TestL2bd, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2bd, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show bridge-domain %s detail" % + self.bd_id)) + + @classmethod + def create_hosts_and_learn(cls, count): + """ + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address. Create + L2 MAC packet stream with host MAC addresses per interface to let + the bridge domain learn these MAC addresses. + + :param count: Integer number of hosts to create MAC/IPv4 addresses for. + """ + n_int = len(cls.pg_interfaces) + macs_per_if = count / n_int + i = -1 + for pg_if in cls.pg_interfaces: + i += 1 + start_nr = macs_per_if * i + end_nr = count if i == (n_int - 1) else macs_per_if * (i + 1) + cls.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = cls.hosts_by_pg_idx[pg_if.sw_if_index] + packets = [] + for j in range(start_nr, end_nr): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac)) + hosts.append(host) + if hasattr(pg_if, 'sub_if'): + packet = pg_if.sub_if.add_dot1_layer(packet) + packets.append(packet) + pg_if.add_stream(packets) + cls.logger.info("Sending broadcast eth frames for MAC learning") + cls.pg_start() + + def create_stream(self, src_if, packet_sizes, packets_per_burst): + """ + Create input packet stream for defined interface. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :param int packets_per_burst: Number of packets in burst. + :return: Stream of packets. + """ + pkts = [] + for i in range(0, packets_per_burst): + dst_if = self.flows[src_if][i % 2] + dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) + src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + if hasattr(src_if, 'sub_if'): + p = src_if.sub_if.add_dot1_layer(p) + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + payload_info = self.payload_to_info(str(packet[Raw])) + src_sw_if_index = payload_info.src + src_if = None + for ifc in self.pg_interfaces: + if ifc != pg_if: + if ifc.sw_if_index == src_sw_if_index: + src_if = ifc + break + if hasattr(src_if, 'sub_if'): + # Check VLAN tags and Ethernet header + packet = src_if.sub_if.remove_dot1_layer(packet) + self.assertTrue(Dot1Q not in packet) + try: + ip = packet[IP] + udp = packet[UDP] + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def run_l2bd_test(self, pkts_per_burst): + """ L2BD MAC learning test """ + + # Create incoming packet streams for packet-generator interfaces + for i in self.pg_interfaces: + packet_sizes = self.sub_if_packet_sizes if hasattr(i, 'sub_if') \ + else self.pg_if_packet_sizes + pkts = self.create_stream(i, packet_sizes, pkts_per_burst) + i.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify outgoing packet streams per packet-generator interface + for i in self.pg_interfaces: + capture = i.get_capture() + self.logger.info("Verifying capture on interface %s" % i.name) + self.verify_capture(i, capture) + + def test_l2bd_sl(self): + """ L2BD MAC learning single-loop test + + Test scenario: + 1.config + MAC learning enabled + learn 100 MAC enries + 3 interfaces: untagged, dot1q, dot1ad (dot1q used instead of + dot1ad in the first version) + + 2.sending l2 eth pkts between 3 interface + 64B, 512B, 1518B, 9200B (ether_size) + burst of 2 pkts per interface + """ + + self.run_l2bd_test(self.sl_pkts_per_burst) + + def test_l2bd_dl(self): + """ L2BD MAC learning dual-loop test + + Test scenario: + 1.config + MAC learning enabled + learn 100 MAC enries + 3 interfaces: untagged, dot1q, dot1ad (dot1q used instead of + dot1ad in the first version) + + 2.sending l2 eth pkts between 3 interface + 64B, 512B, 1518B, 9200B (ether_size) + burst of 257 pkts per interface + """ + + self.run_l2bd_test(self.dl_pkts_per_burst) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2bd_arp_term.py b/test/test_l2bd_arp_term.py new file mode 100644 index 00000000..20cc537e --- /dev/null +++ b/test/test_l2bd_arp_term.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python +""" L2BD ARP term Test """ + +import unittest +import random +import copy + +from socket import AF_INET, AF_INET6 + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet import IP +from scapy.utils import inet_pton, inet_ntop +from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ptop, in6_islladdr, \ + in6_mactoifaceid, in6_ismaddr +from scapy.layers.inet6 import IPv6, UDP, ICMPv6ND_NS, ICMPv6ND_RS, \ + ICMPv6ND_RA, ICMPv6NDOptSrcLLAddr, getmacbyip6, ICMPv6MRD_Solicitation, \ + ICMPv6NDOptMTU, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptPrefixInfo, \ + ICMPv6ND_NA, ICMPv6NDOptDstLLAddr, ICMPv6DestUnreach, icmp6types + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp, mactobinary + + +class TestL2bdArpTerm(VppTestCase): + """ L2BD arp termination Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestL2bdArpTerm, cls).setUpClass() + + try: + # Create pg interfaces + n_bd = 1 + cls.ifs_per_bd = ifs_per_bd = 3 + n_ifs = n_bd * ifs_per_bd + cls.create_pg_interfaces(range(n_ifs)) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + cls.hosts = set() + + except Exception: + super(TestL2bdArpTerm, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + self.reset_packet_infos() + super(TestL2bdArpTerm, self).setUp() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2bdArpTerm, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show bridge-domain 1 detail")) + + def add_del_arp_term_hosts(self, entries, bd_id=1, is_add=1, is_ipv6=0): + for e in entries: + ip = e.ip4n if is_ipv6 == 0 else e.ip6n + self.vapi.bd_ip_mac_add_del(bd_id=bd_id, + mac=e.bin_mac, + ip=ip, + is_ipv6=is_ipv6, + is_add=is_add) + + @classmethod + def mac_list(cls, b6_range): + return ["00:00:ca:fe:00:%02x" % b6 for b6 in b6_range] + + @classmethod + def ip4_host(cls, subnet, host, mac): + return Host(mac=mac, + ip4="172.17.1%02u.%u" % (subnet, host)) + + @classmethod + def ip4_hosts(cls, subnet, start, mac_list): + return {cls.ip4_host(subnet, start + j, mac_list[j]) + for j in range(len(mac_list))} + + @classmethod + def ip6_host(cls, subnet, host, mac): + return Host(mac=mac, + ip6="fd01:%x::%x" % (subnet, host)) + + @classmethod + def ip6_hosts(cls, subnet, start, mac_list): + return {cls.ip6_host(subnet, start + j, mac_list[j]) + for j in range(len(mac_list))} + + @classmethod + def bd_swifs(cls, b): + n = cls.ifs_per_bd + start = (b - 1) * n + return [cls.pg_interfaces[j] for j in range(start, start + n)] + + def bd_add_del(self, bd_id=1, is_add=1): + if is_add: + self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add) + for swif in self.bd_swifs(bd_id): + swif_idx = swif.sw_if_index + self.vapi.sw_interface_set_l2_bridge( + swif_idx, bd_id=bd_id, enable=is_add) + if not is_add: + self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add) + + @classmethod + def arp_req(cls, src_host, host): + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=src_host.mac) / + ARP(op="who-has", + hwsrc=src_host.bin_mac, + pdst=host.ip4, + psrc=src_host.ip4)) + + @classmethod + def arp_reqs(cls, src_host, entries): + return [cls.arp_req(src_host, e) for e in entries] + + @classmethod + def garp_req(cls, host): + return cls.arp_req(host, host) + + @classmethod + def garp_reqs(cls, entries): + return [cls.garp_req(e) for e in entries] + + def arp_resp_host(self, src_host, arp_resp): + ether = arp_resp[Ether] + self.assertEqual(ether.dst, src_host.mac) + + arp = arp_resp[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + arp_opts = {"who-has": 1, "is-at": 2} + self.assertEqual(arp.op, arp_opts["is-at"]) + self.assertEqual(arp.hwdst, src_host.mac) + self.assertEqual(arp.pdst, src_host.ip4) + return Host(mac=arp.hwsrc, ip4=arp.psrc) + + def arp_resp_hosts(self, src_host, pkts): + return {self.arp_resp_host(src_host, p) for p in pkts} + + def inttoip4(self, ip): + o1 = int(ip / 16777216) % 256 + o2 = int(ip / 65536) % 256 + o3 = int(ip / 256) % 256 + o4 = int(ip) % 256 + return '%(o1)s.%(o2)s.%(o3)s.%(o4)s' % locals() + + def arp_event_host(self, e): + return Host(mac=':'.join(['%02x' % ord(char) for char in e.new_mac]), + ip4=self.inttoip4(e.address)) + + def arp_event_hosts(self, evs): + return {self.arp_event_host(e) for e in evs} + + def nd_event_host(self, e): + return Host(mac=':'.join(['%02x' % ord(char) for char in e.new_mac]), + ip6=inet_ntop(AF_INET6, e.address)) + + def nd_event_hosts(self, evs): + return {self.nd_event_host(e) for e in evs} + + @classmethod + def ns_req(cls, src_host, host): + nsma = in6_getnsma(inet_pton(AF_INET6, "fd10::ffff")) + d = inet_ntop(AF_INET6, nsma) + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=src_host.mac) / + IPv6(dst=d, src=src_host.ip6) / + ICMPv6ND_NS(tgt=host.ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=src_host.mac)) + + @classmethod + def ns_reqs_dst(cls, entries, dst_host): + return [cls.ns_req(e, dst_host) for e in entries] + + @classmethod + def ns_reqs_src(cls, src_host, entries): + return [cls.ns_req(src_host, e) for e in entries] + + def na_resp_host(self, src_host, rx): + self.assertEqual(rx[Ether].dst, src_host.mac) + self.assertEqual(in6_ptop(rx[IPv6].dst), + in6_ptop(src_host.ip6)) + + self.assertTrue(rx.haslayer(ICMPv6ND_NA)) + self.assertTrue(rx.haslayer(ICMPv6NDOptDstLLAddr)) + + na = rx[ICMPv6ND_NA] + return Host(mac=na.lladdr, ip6=na.tgt) + + def na_resp_hosts(self, src_host, pkts): + return {self.na_resp_host(src_host, p) for p in pkts} + + def set_bd_flags(self, bd_id, **args): + """ + Enable/disable defined feature(s) of the bridge domain. + + :param int bd_id: Bridge domain ID. + :param list args: List of feature/status pairs. Allowed features: \ + learn, forward, flood, uu_flood and arp_term. Status False means \ + disable, status True means enable the feature. + :raise: ValueError in case of unknown feature in the input. + """ + for flag in args: + if flag == "learn": + feature_bitmap = 1 << 0 + elif flag == "forward": + feature_bitmap = 1 << 1 + elif flag == "flood": + feature_bitmap = 1 << 2 + elif flag == "uu_flood": + feature_bitmap = 1 << 3 + elif flag == "arp_term": + feature_bitmap = 1 << 4 + else: + raise ValueError("Unknown feature used: %s" % flag) + is_set = 1 if args[flag] else 0 + self.vapi.bridge_flags(bd_id, is_set, feature_bitmap) + self.logger.info("Bridge domain ID %d updated" % bd_id) + + def verify_arp(self, src_host, req_hosts, resp_hosts, bd_id=1): + reqs = self.arp_reqs(src_host, req_hosts) + + for swif in self.bd_swifs(bd_id): + swif.add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + for swif in self.bd_swifs(bd_id): + resp_pkts = swif.get_capture(len(resp_hosts)) + resps = self.arp_resp_hosts(src_host, resp_pkts) + self.assertEqual(len(resps ^ resp_hosts), 0) + + def verify_nd(self, src_host, req_hosts, resp_hosts, bd_id=1): + reqs = self.ns_reqs_src(src_host, req_hosts) + + for swif in self.bd_swifs(bd_id): + swif.add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + for swif in self.bd_swifs(bd_id): + resp_pkts = swif.get_capture(len(resp_hosts)) + resps = self.na_resp_hosts(src_host, resp_pkts) + self.assertEqual(len(resps ^ resp_hosts), 0) + + def test_l2bd_arp_term_01(self): + """ L2BD arp term - add 5 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(1, 5)) + hosts = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(hosts, is_add=1) + self.verify_arp(src_host, hosts, hosts) + type(self).hosts = hosts + + def test_l2bd_arp_term_02(self): + """ L2BD arp term - delete 3 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(1, 3)) + deleted = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(deleted, is_add=0) + remaining = self.hosts - deleted + self.verify_arp(src_host, self.hosts, remaining) + type(self).hosts = remaining + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_03(self): + """ L2BD arp term - recreate BD1, readd 3 hosts, verify arp responses + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(1, 3)) + readded = self.ip4_hosts(4, 1, macs) + self.add_del_arp_term_hosts(readded, is_add=1) + self.verify_arp(src_host, self.hosts | readded, readded) + type(self).hosts = readded + + def test_l2bd_arp_term_04(self): + """ L2BD arp term - 2 IP4 addrs per host + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(1, 3)) + sub5_hosts = self.ip4_hosts(5, 1, macs) + self.add_del_arp_term_hosts(sub5_hosts, is_add=1) + hosts = self.hosts | sub5_hosts + self.verify_arp(src_host, hosts, hosts) + type(self).hosts = hosts + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_05(self): + """ L2BD arp term - create and update 10 IP4-mac pairs + """ + src_host = self.ip4_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs1 = self.mac_list(range(10, 20)) + hosts1 = self.ip4_hosts(5, 1, macs1) + self.add_del_arp_term_hosts(hosts1, is_add=1) + self.verify_arp(src_host, hosts1, hosts1) + macs2 = self.mac_list(range(20, 30)) + hosts2 = self.ip4_hosts(5, 1, macs2) + self.add_del_arp_term_hosts(hosts2, is_add=1) + self.verify_arp(src_host, hosts1, hosts2) + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_06(self): + """ L2BD arp/ND term - hosts with both ip4/ip6 + """ + src_host4 = self.ip4_host(50, 50, "00:00:11:22:33:44") + src_host6 = self.ip6_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + # enable flood to make sure requests are not flooded + self.set_bd_flags(1, arp_term=True, flood=True, + uu_flood=False, learn=False) + macs = self.mac_list(range(10, 20)) + hosts6 = self.ip6_hosts(5, 1, macs) + hosts4 = self.ip4_hosts(5, 1, macs) + self.add_del_arp_term_hosts(hosts4, is_add=1) + self.add_del_arp_term_hosts(hosts6, is_add=1, is_ipv6=1) + self.verify_arp(src_host4, hosts4, hosts4) + self.verify_nd(src_host6, hosts6, hosts6) + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_07(self): + """ L2BD ND term - Add and Del hosts, verify ND replies + """ + src_host6 = self.ip6_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(10, 20)) + hosts6 = self.ip6_hosts(5, 1, macs) + self.add_del_arp_term_hosts(hosts6, is_add=1, is_ipv6=1) + self.verify_nd(src_host6, hosts6, hosts6) + del_macs = self.mac_list(range(10, 15)) + deleted = self.ip6_hosts(5, 1, del_macs) + self.add_del_arp_term_hosts(deleted, is_add=0, is_ipv6=1) + self.verify_nd(src_host6, hosts6, hosts6 - deleted) + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_08(self): + """ L2BD ND term - Add and update IP+mac, verify ND replies + """ + src_host = self.ip6_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs1 = self.mac_list(range(10, 20)) + hosts = self.ip6_hosts(5, 1, macs1) + self.add_del_arp_term_hosts(hosts, is_add=1, is_ipv6=1) + self.verify_nd(src_host, hosts, hosts) + macs2 = self.mac_list(range(20, 30)) + updated = self.ip6_hosts(5, 1, macs2) + self.add_del_arp_term_hosts(updated, is_add=1, is_ipv6=1) + self.verify_nd(src_host, hosts, updated) + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_09(self): + """ L2BD arp term - send garps, verify arp event reports + """ + self.vapi.want_ip4_arp_events() + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(90, 95)) + hosts = self.ip4_hosts(5, 1, macs) + + garps = self.garp_reqs(hosts) + self.bd_swifs(1)[0].add_stream(garps) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + evs = [self.vapi.wait_for_event(1, "ip4_arp_event") + for i in range(len(hosts))] + ev_hosts = self.arp_event_hosts(evs) + self.assertEqual(len(ev_hosts ^ hosts), 0) + + def test_l2bd_arp_term_10(self): + """ L2BD arp term - send duplicate garps, verify suppression + """ + macs = self.mac_list(range(70, 71)) + hosts = self.ip4_hosts(6, 1, macs) + + """ send the packet 5 times expect one event + """ + garps = self.garp_reqs(hosts) * 5 + self.bd_swifs(1)[0].add_stream(garps) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + evs = [self.vapi.wait_for_event(1, "ip4_arp_event") + for i in range(len(hosts))] + ev_hosts = self.arp_event_hosts(evs) + self.assertEqual(len(ev_hosts ^ hosts), 0) + + def test_l2bd_arp_term_11(self): + """ L2BD arp term - disable ip4 arp events,send garps, verify no events + """ + self.vapi.want_ip4_arp_events(enable_disable=0) + macs = self.mac_list(range(90, 95)) + hosts = self.ip4_hosts(5, 1, macs) + + garps = self.garp_reqs(hosts) + self.bd_swifs(1)[0].add_stream(garps) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.sleep(1) + self.assertEqual(len(self.vapi.collect_events()), 0) + self.bd_add_del(1, is_add=0) + + def test_l2bd_arp_term_12(self): + """ L2BD ND term - send NS packets verify reports + """ + self.vapi.want_ip6_nd_events(address=inet_pton(AF_INET6, "::0")) + dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44") + self.bd_add_del(1, is_add=1) + self.set_bd_flags(1, arp_term=True, flood=False, + uu_flood=False, learn=False) + macs = self.mac_list(range(10, 15)) + hosts = self.ip6_hosts(5, 1, macs) + reqs = self.ns_reqs_dst(hosts, dst_host) + self.bd_swifs(1)[0].add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + evs = [self.vapi.wait_for_event(2, "ip6_nd_event") + for i in range(len(hosts))] + ev_hosts = self.nd_event_hosts(evs) + self.assertEqual(len(ev_hosts ^ hosts), 0) + + def test_l2bd_arp_term_13(self): + """ L2BD ND term - send duplicate ns, verify suppression + """ + dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(10, 11)) + hosts = self.ip6_hosts(5, 1, macs) + reqs = self.ns_reqs_dst(hosts, dst_host) * 5 + self.bd_swifs(1)[0].add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + evs = [self.vapi.wait_for_event(2, "ip6_nd_event") + for i in range(len(hosts))] + ev_hosts = self.nd_event_hosts(evs) + self.assertEqual(len(ev_hosts ^ hosts), 0) + + def test_l2bd_arp_term_14(self): + """ L2BD ND term - disable ip4 arp events,send ns, verify no events + """ + self.vapi.want_ip6_nd_events(enable_disable=0, + address=inet_pton(AF_INET6, "::0")) + dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44") + macs = self.mac_list(range(10, 15)) + hosts = self.ip6_hosts(5, 1, macs) + reqs = self.ns_reqs_dst(hosts, dst_host) + self.bd_swifs(1)[0].add_stream(reqs) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.sleep(1) + self.assertEqual(len(self.vapi.collect_events()), 0) + self.bd_add_del(1, is_add=0) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2bd_multi_instance.py b/test/test_l2bd_multi_instance.py new file mode 100644 index 00000000..04ba570c --- /dev/null +++ b/test/test_l2bd_multi_instance.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python +"""L2BD Multi-instance Test Case HLD: + +**NOTES:** + - higher number of pg-l2 interfaces causes problems => only 15 pg-l2 \ + interfaces in 5 bridge domains are tested + - jumbo packets in configuration with 14 l2-pg interfaces leads to \ + problems too + +**config 1** + - add 15 pg-l2 interfaces + - configure one host per pg-l2 interface + - configure 5 bridge domains (BD) + - add 3 pg-l2 interfaces per BD + +**test 1** + - send L2 MAC frames between all pg-l2 interfaces of all BDs + +**verify 1** + - check BD data by parsing output of bridge_domain_dump API command + - all packets received correctly + +**config 2** + - update data of 5 BD + - disable learning, forwarding, flooding and uu_flooding for BD1 + - disable forwarding for BD2 + - disable flooding for BD3 + - disable uu_flooding for BD4 + - disable learning for BD5 + +**verify 2** + - check BD data by parsing output of bridge_domain_dump API command + +**config 3** + - delete 2 BDs + +**test 3** + - send L2 MAC frames between all pg-l2 interfaces of all BDs + - send L2 MAC frames between all pg-l2 interfaces formerly assigned to \ + deleted BDs + +**verify 3** + - check BD data by parsing output of bridge_domain_dump API command + - all packets received correctly on all 3 pg-l2 interfaces assigned to BDs + - no packet received on all 3 pg-l2 interfaces of all deleted BDs + +**config 4** + - add 2 BDs + - add 3 pg-l2 interfaces per BD + +**test 4** + - send L2 MAC frames between all pg-l2 interfaces of all BDs + +**verify 4** + - check BD data by parsing output of bridge_domain_dump API command + - all packets received correctly + +**config 5** + - delete 5 BDs + +**verify 5** + - check BD data by parsing output of bridge_domain_dump API command +""" + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner, running_extended_tests +from util import Host, ppp + + +class TestL2bdMultiInst(VppTestCase): + """ L2BD Multi-instance Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestL2bdMultiInst, cls).setUpClass() + + try: + # Create pg interfaces + n_bd = 5 + cls.ifs_per_bd = ifs_per_bd = 3 + n_ifs = n_bd * ifs_per_bd + cls.create_pg_interfaces(range(n_ifs)) + + # Packet flows mapping pg0 -> pg1, pg2 etc. + cls.flows = dict() + for b in range(n_bd): + bd_ifs = cls.bd_if_range(b + 1) + for j in bd_ifs: + cls.flows[cls.pg_interfaces[j]] = [ + cls.pg_interfaces[x] for x in bd_ifs if x != j] + assert( + len(cls.flows[cls.pg_interfaces[j]]) == ifs_per_bd - 1) + + # Mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + + # Create test host entries + cls.create_hosts(5) + + # Packet sizes - jumbo packet (9018 bytes) skipped + cls.pg_if_packet_sizes = [64, 512, 1518] + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + # Create list of BDs + cls.bd_list = list() + + # Create list of deleted BDs + cls.bd_deleted_list = list() + + # Create list of pg_interfaces in BDs + cls.pg_in_bd = list() + + except Exception: + super(TestL2bdMultiInst, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + self.reset_packet_infos() + super(TestL2bdMultiInst, self).setUp() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2bdMultiInst, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show bridge-domain")) + + @classmethod + def create_hosts(cls, hosts_per_if): + """ + Create required number of host MAC addresses and distribute them + among interfaces. Create host IPv4 address for every host MAC + address. + + :param int hosts_per_if: Number of hosts per if to create MAC/IPv4 + addresses for. + """ + c = hosts_per_if + assert(not cls.hosts_by_pg_idx) + for i in range(len(cls.pg_interfaces)): + pg_idx = cls.pg_interfaces[i].sw_if_index + cls.hosts_by_pg_idx[pg_idx] = [Host( + "00:00:00:ff:%02x:%02x" % (pg_idx, j + 1), + "172.17.1%02u.%u" % (pg_idx, j + 1)) for j in range(c)] + + @classmethod + def bd_if_range(cls, b): + n = cls.ifs_per_bd + start = (b - 1) * n + return range(start, start + n) + + def create_bd_and_mac_learn(self, count, start=1): + """ + Create required number of bridge domains with MAC learning enabled, + put 3 l2-pg interfaces to every bridge domain and send MAC learning + packets. + + :param int count: Number of bridge domains to be created. + :param int start: Starting number of the bridge domain ID. + (Default value = 1) + """ + for b in range(start, start + count): + self.vapi.bridge_domain_add_del(bd_id=b) + self.logger.info("Bridge domain ID %d created" % b) + if self.bd_list.count(b) == 0: + self.bd_list.append(b) + if self.bd_deleted_list.count(b) == 1: + self.bd_deleted_list.remove(b) + for j in self.bd_if_range(b): + pg_if = self.pg_interfaces[j] + self.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, + bd_id=b) + self.logger.info("pg-interface %s added to bridge domain ID %d" + % (pg_if.name, b)) + self.pg_in_bd.append(pg_if) + hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] + packets = [Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac) + for host in hosts] + pg_if.add_stream(packets) + self.logger.info("Sending broadcast eth frames for MAC learning") + self.pg_start() + self.logger.info(self.vapi.ppcli("show bridge-domain")) + self.logger.info(self.vapi.ppcli("show l2fib")) + + def delete_bd(self, count, start=1): + """ + Delete required number of bridge domains. + + :param int count: Number of bridge domains to be created. + :param int start: Starting number of the bridge domain ID. + (Default value = 1) + """ + for b in range(start, start + count): + for j in self.bd_if_range(b): + pg_if = self.pg_interfaces[j] + self.vapi.sw_interface_set_l2_bridge(pg_if.sw_if_index, + bd_id=b, enable=0) + self.pg_in_bd.remove(pg_if) + self.vapi.bridge_domain_add_del(bd_id=b, is_add=0) + self.bd_list.remove(b) + self.bd_deleted_list.append(b) + self.logger.info("Bridge domain ID %d deleted" % b) + + def create_stream(self, src_if): + """ + Create input packet stream for defined interface using hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :return: Stream of packets. + """ + packet_sizes = self.pg_if_packet_sizes + pkts = [] + src_hosts = self.hosts_by_pg_idx[src_if.sw_if_index] + for dst_if in self.flows[src_if]: + dst_hosts = self.hosts_by_pg_idx[dst_if.sw_if_index] + for dst_host in dst_hosts: + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + src_host = random.choice(src_hosts) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + self.logger.debug("Input stream created for port %s. Length: %u pkt(s)" + % (src_if.name, len(pkts))) + return pkts + + def verify_capture(self, dst_if): + """ + Verify captured input packet stream for defined interface. + + :param object dst_if: Interface to verify captured packet stream for. + """ + last_info = dict() + for i in self.flows[dst_if]: + last_info[i.sw_if_index] = None + dst = dst_if.sw_if_index + for packet in dst_if.get_capture(): + try: + ip = packet[IP] + udp = packet[UDP] + info = self.payload_to_info(str(packet[Raw])) + self.assertEqual(info.dst, dst) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, info.src, info.index)) + last_info[info.src] = self.get_next_packet_info_for_interface2( + info.src, dst, last_info[info.src]) + pkt_info = last_info[info.src] + self.assertTrue(pkt_info is not None) + self.assertEqual(info.index, pkt_info.index) + # Check standard fields against saved data in pkt + saved = pkt_info.data + self.assertEqual(ip.src, saved[IP].src) + self.assertEqual(ip.dst, saved[IP].dst) + self.assertEqual(udp.sport, saved[UDP].sport) + self.assertEqual(udp.dport, saved[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + s = "" + remaining = 0 + for src in self.flows[dst_if]: + remaining_packet = self.get_next_packet_info_for_interface2( + src.sw_if_index, dst, last_info[src.sw_if_index]) + if remaining_packet is None: + s += "Port %u: Packet expected from source %u didn't arrive\n"\ + % (dst, src.sw_if_index) + remaining += 1 + self.assertNotEqual(0, remaining, s) + + def set_bd_flags(self, bd_id, **args): + """ + Enable/disable defined feature(s) of the bridge domain. + + :param int bd_id: Bridge domain ID. + :param list args: List of feature/status pairs. Allowed features: \ + learn, forward, flood, uu_flood and arp_term. Status False means \ + disable, status True means enable the feature. + :raise: ValueError in case of unknown feature in the input. + """ + for flag in args: + if flag == "learn": + feature_bitmap = 1 << 0 + elif flag == "forward": + feature_bitmap = 1 << 1 + elif flag == "flood": + feature_bitmap = 1 << 2 + elif flag == "uu_flood": + feature_bitmap = 1 << 3 + elif flag == "arp_term": + feature_bitmap = 1 << 4 + else: + raise ValueError("Unknown feature used: %s" % flag) + is_set = 1 if args[flag] else 0 + self.vapi.bridge_flags(bd_id, is_set, feature_bitmap) + self.logger.info("Bridge domain ID %d updated" % bd_id) + + def verify_bd(self, bd_id, **args): + """ + Check if the bridge domain is configured and verify expected status + of listed features. + + :param int bd_id: Bridge domain ID. + :param list args: List of feature/status pairs. Allowed features: \ + learn, forward, flood, uu_flood and arp_term. Status False means \ + disable, status True means enable the feature. + :return: 1 if bridge domain is configured, otherwise return 0. + :raise: ValueError in case of unknown feature in the input. + """ + bd_dump = self.vapi.bridge_domain_dump(bd_id) + if len(bd_dump) == 0: + self.logger.info("Bridge domain ID %d is not configured" % bd_id) + return 0 + else: + bd_dump = bd_dump[0] + if len(args) > 0: + for flag in args: + expected_status = 1 if args[flag] else 0 + if flag == "learn": + flag_status = bd_dump[6] + elif flag == "forward": + flag_status = bd_dump[5] + elif flag == "flood": + flag_status = bd_dump[3] + elif flag == "uu_flood": + flag_status = bd_dump[4] + elif flag == "arp_term": + flag_status = bd_dump[7] + else: + raise ValueError("Unknown feature used: %s" % flag) + self.assertEqual(expected_status, flag_status) + return 1 + + def run_verify_test(self): + """ + Create packet streams for all configured l2-pg interfaces, send all \ + prepared packet streams and verify that: + - all packets received correctly on all pg-l2 interfaces assigned + to bridge domains + - no packet received on all pg-l2 interfaces not assigned to + bridge domains + + :raise RuntimeError: if no packet captured on l2-pg interface assigned + to the bridge domain or if any packet is captured + on l2-pg interface not assigned to the bridge + domain. + """ + # Test + # Create incoming packet streams for packet-generator interfaces + # for pg_if in self.pg_interfaces: + assert(len(self._packet_count_for_dst_if_idx) == 0) + for pg_if in self.pg_in_bd: + pkts = self.create_stream(pg_if) + pg_if.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_in_bd) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for pg_if in self.pg_in_bd: + self.verify_capture(pg_if) + + def test_l2bd_inst_01(self): + """ L2BD Multi-instance test 1 - create 5 BDs + """ + # Config 1 + # Create 5 BDs, put interfaces to these BDs and send MAC learning + # packets + self.create_bd_and_mac_learn(5) + + # Verify 1 + for bd_id in self.bd_list: + self.assertEqual(self.verify_bd(bd_id), 1) + + # Test 1 + # self.vapi.cli("clear trace") + self.run_verify_test() + + def test_l2bd_inst_02(self): + """ L2BD Multi-instance test 2 - update data of 5 BDs + """ + # Config 2 + # Update data of 5 BDs (disable learn, forward, flood, uu-flood) + self.set_bd_flags(self.bd_list[0], learn=False, forward=False, + flood=False, uu_flood=False) + self.set_bd_flags(self.bd_list[1], forward=False) + self.set_bd_flags(self.bd_list[2], flood=False) + self.set_bd_flags(self.bd_list[3], uu_flood=False) + self.set_bd_flags(self.bd_list[4], learn=False) + + # Verify 2 + # Skipping check of uu_flood as it is not returned by + # bridge_domain_dump api command + self.verify_bd(self.bd_list[0], learn=False, forward=False, + flood=False, uu_flood=False) + self.verify_bd(self.bd_list[1], learn=True, forward=False, + flood=True, uu_flood=True) + self.verify_bd(self.bd_list[2], learn=True, forward=True, + flood=False, uu_flood=True) + self.verify_bd(self.bd_list[3], learn=True, forward=True, + flood=True, uu_flood=False) + self.verify_bd(self.bd_list[4], learn=False, forward=True, + flood=True, uu_flood=True) + + def test_l2bd_inst_03(self): + """ L2BD Multi-instance test 3 - delete 2 BDs + """ + # Config 3 + # Delete 2 BDs + self.delete_bd(2) + + # Verify 3 + for bd_id in self.bd_deleted_list: + self.assertEqual(self.verify_bd(bd_id), 0) + for bd_id in self.bd_list: + self.assertEqual(self.verify_bd(bd_id), 1) + + # Test 3 + self.run_verify_test() + + def test_l2bd_inst_04(self): + """ L2BD Multi-instance test 4 - add 2 BDs + """ + # Config 4 + # Create 5 BDs, put interfaces to these BDs and send MAC learning + # packets + self.create_bd_and_mac_learn(2) + + # Verify 4 + for bd_id in self.bd_list: + self.assertEqual(self.verify_bd(bd_id), 1) + + # Test 4 + # self.vapi.cli("clear trace") + self.run_verify_test() + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_l2bd_inst_05(self): + """ L2BD Multi-instance test 5 - delete 5 BDs + """ + # Config 5 + # Delete 5 BDs + self.delete_bd(5) + + # Verify 5 + for bd_id in self.bd_deleted_list: + self.assertEqual(self.verify_bd(bd_id), 0) + for bd_id in self.bd_list: + self.assertEqual(self.verify_bd(bd_id), 1) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2xc.py b/test/test_l2xc.py new file mode 100644 index 00000000..2ec4af92 --- /dev/null +++ b/test/test_l2xc.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp + + +class TestL2xc(VppTestCase): + """ L2XC Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + + :var int hosts_nr: Number of hosts to be created. + :var int dl_pkts_per_burst: Number of packets in burst for dual-loop + test. + :var int sl_pkts_per_burst: Number of packets in burst for single-loop + test. + """ + super(TestL2xc, cls).setUpClass() + + # Test variables + cls.hosts_nr = 10 + cls.dl_pkts_per_burst = 257 + cls.sl_pkts_per_burst = 2 + + try: + # create 4 pg interfaces + cls.create_pg_interfaces(range(4)) + + # packet flows mapping pg0 -> pg1, pg2 -> pg3, etc. + cls.flows = dict() + cls.flows[cls.pg0] = [cls.pg1] + cls.flows[cls.pg1] = [cls.pg0] + cls.flows[cls.pg2] = [cls.pg3] + cls.flows[cls.pg3] = [cls.pg2] + + # packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + cls.interfaces = list(cls.pg_interfaces) + + # Create bi-directional cross-connects between pg0 and pg1 + cls.vapi.sw_interface_set_l2_xconnect( + cls.pg0.sw_if_index, cls.pg1.sw_if_index, enable=1) + cls.vapi.sw_interface_set_l2_xconnect( + cls.pg1.sw_if_index, cls.pg0.sw_if_index, enable=1) + + # Create bi-directional cross-connects between pg2 and pg3 + cls.vapi.sw_interface_set_l2_xconnect( + cls.pg2.sw_if_index, cls.pg3.sw_if_index, enable=1) + cls.vapi.sw_interface_set_l2_xconnect( + cls.pg3.sw_if_index, cls.pg2.sw_if_index, enable=1) + + # mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + + # Create host MAC and IPv4 lists + cls.create_host_lists(cls.hosts_nr) + + # setup all interfaces + for i in cls.interfaces: + i.admin_up() + + except Exception: + super(TestL2xc, cls).tearDownClass() + raise + + def setUp(self): + super(TestL2xc, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2xc, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2patch")) + + @classmethod + def create_host_lists(cls, count): + """ + Method to create required number of MAC and IPv4 addresses. + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address too. + + :param count: Number of hosts to create MAC and IPv4 addresses for. + """ + for pg_if in cls.pg_interfaces: + cls.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = cls.hosts_by_pg_idx[pg_if.sw_if_index] + for j in range(0, count): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + hosts.append(host) + + def create_stream(self, src_if, packet_sizes, packets_per_burst): + """ + Create input packet stream for defined interface. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :param int packets_per_burst: Number of packets in burst. + :return: Stream of packets. + """ + pkts = [] + for i in range(0, packets_per_burst): + dst_if = self.flows[src_if][0] + dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) + src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + try: + ip = packet[IP] + udp = packet[UDP] + payload_info = self.payload_to_info(str(packet[Raw])) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Port %u: Packet expected from source %u didn't" + " arrive" % (dst_sw_if_index, i.sw_if_index)) + + def run_l2xc_test(self, pkts_per_burst): + """ L2XC test """ + + # Create incoming packet streams for packet-generator interfaces + for i in self.interfaces: + pkts = self.create_stream(i, self.pg_if_packet_sizes, + pkts_per_burst) + i.add_stream(pkts) + + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify outgoing packet streams per packet-generator interface + for i in self.pg_interfaces: + capture = i.get_capture() + self.logger.info("Verifying capture on interface %s" % i.name) + self.verify_capture(i, capture) + + def test_l2xc_sl(self): + """ L2XC single-loop test + + Test scenario: + 1. config + 2 pairs of 2 interfaces, l2xconnected + + 2. sending l2 eth packets between 4 interfaces + 64B, 512B, 1518B, 9018B (ether_size) + burst of 2 packets per interface + """ + + self.run_l2xc_test(self.sl_pkts_per_burst) + + def test_l2xc_dl(self): + """ L2XC dual-loop test + + Test scenario: + 1. config + 2 pairs of 2 interfaces, l2xconnected + + 2. sending l2 eth packets between 4 interfaces + 64B, 512B, 1518B, 9018B (ether_size) + burst of 257 packets per interface + """ + + self.run_l2xc_test(self.dl_pkts_per_burst) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_l2xc_multi_instance.py b/test/test_l2xc_multi_instance.py new file mode 100644 index 00000000..bb26f959 --- /dev/null +++ b/test/test_l2xc_multi_instance.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python +"""L2XC Multi-instance Test Case HLD: + +**NOTES:** + - higher number (more than 15) of pg-l2 interfaces causes problems => only + 14 pg-l2 interfaces and 10 cross-connects are tested + - jumbo packets in configuration with 14 l2-pg interfaces leads to + problems too + +**config 1** + - add 14 pg-l2 interfaces + - add 10 cross-connects (two cross-connects per pair of l2-pg interfaces) + +**test 1** + - send L2 MAC frames between all pairs of pg-l2 interfaces + +**verify 1** + - all packets received correctly in case of cross-connected l2-pg + interfaces + - no packet received in case of not cross-connected l2-pg interfaces + +**config 2** + - delete 4 cross-connects + +**test 2** + - send L2 MAC frames between all pairs of pg-l2 interfaces + +**verify 2** + - all packets received correctly in case of cross-connected l2-pg + interfaces + - no packet received in case of not cross-connected l2-pg interfaces + +**config 3** + - add new 4 cross-connects + +**test 3** + - send L2 MAC frames between all pairs of pg-l2 interfaces + +**verify 3** + - all packets received correctly in case of cross-connected l2-pg + interfaces + - no packet received in case of not cross-connected l2-pg interfaces + +**config 4** + - delete 10 cross-connects + +**test 4** + - send L2 MAC frames between all pairs of pg-l2 interfaces + +**verify 4** + - no packet received on all of l2-pg interfaces (no cross-connect created) +""" + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp + + +class TestL2xcMultiInst(VppTestCase): + """ L2XC Multi-instance Test Case """ + + @classmethod + def setUpClass(cls): + """ + Perform standard class setup (defined by class method setUpClass in + class VppTestCase) before running the test case, set test case related + variables and configure VPP. + """ + super(TestL2xcMultiInst, cls).setUpClass() + + try: + # Create pg interfaces + cls.create_pg_interfaces(range(14)) + + # Packet flows mapping pg0 -> pg1 etc. + cls.flows = dict() + for i in range(len(cls.pg_interfaces)): + delta = 1 if i % 2 == 0 else -1 + cls.flows[cls.pg_interfaces[i]] =\ + [cls.pg_interfaces[i + delta]] + + # Mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + for pg_if in cls.pg_interfaces: + cls.hosts_by_pg_idx[pg_if.sw_if_index] = [] + + # Create test host entries + cls.create_hosts(70) + + # Packet sizes - jumbo packet (9018 bytes) skipped + cls.pg_if_packet_sizes = [64, 512, 1518] + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + # Create list of x-connected pg_interfaces + cls.pg_in_xc = list() + + # Create list of not x-connected pg_interfaces + cls.pg_not_in_xc = list() + for pg_if in cls.pg_interfaces: + cls.pg_not_in_xc.append(pg_if) + + except Exception: + super(TestL2xcMultiInst, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + super(TestL2xcMultiInst, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestL2xcMultiInst, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2patch")) + + @classmethod + def create_hosts(cls, count): + """ + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address. + + :param int count: Number of hosts to create MAC/IPv4 addresses for. + """ + n_int = len(cls.pg_interfaces) + macs_per_if = count / n_int + i = -1 + for pg_if in cls.pg_interfaces: + i += 1 + start_nr = macs_per_if * i + end_nr = count if i == (n_int - 1) else macs_per_if * (i + 1) + hosts = cls.hosts_by_pg_idx[pg_if.sw_if_index] + for j in range(start_nr, end_nr): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02u.%u" % (pg_if.sw_if_index, j)) + hosts.append(host) + + def create_xconnects(self, count, start=0): + """ + Create required number of cross-connects (always two cross-connects per + pair of packet-generator interfaces). + + :param int count: Number of cross-connects to be created. + :param int start: Starting index of packet-generator interfaces. \ + (Default value = 0) + """ + for i in range(count): + rx_if = self.pg_interfaces[i + start] + delta = 1 if i % 2 == 0 else -1 + tx_if = self.pg_interfaces[i + start + delta] + self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index, + tx_if.sw_if_index, 1) + self.logger.info("Cross-connect from %s to %s created" + % (tx_if.name, rx_if.name)) + if self.pg_in_xc.count(rx_if) == 0: + self.pg_in_xc.append(rx_if) + if self.pg_not_in_xc.count(rx_if) == 1: + self.pg_not_in_xc.remove(rx_if) + + def delete_xconnects(self, count, start=0): + """ + Delete required number of cross-connects (always two cross-connects per + pair of packet-generator interfaces). + + :param int count: Number of cross-connects to be deleted. + :param int start: Starting index of packet-generator interfaces. \ + (Default value = 0) + """ + for i in range(count): + rx_if = self.pg_interfaces[i + start] + delta = 1 if i % 2 == 0 else -1 + tx_if = self.pg_interfaces[i + start + delta] + self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index, + tx_if.sw_if_index, 0) + self.logger.info("Cross-connect from %s to %s deleted" + % (tx_if.name, rx_if.name)) + if self.pg_not_in_xc.count(rx_if) == 0: + self.pg_not_in_xc.append(rx_if) + if self.pg_in_xc.count(rx_if) == 1: + self.pg_in_xc.remove(rx_if) + + def create_stream(self, src_if, packet_sizes): + """ + Create input packet stream for defined interface using hosts list. + + :param object src_if: Interface to create packet stream for. + :param list packet_sizes: List of required packet sizes. + :return: Stream of packets. + """ + pkts = [] + src_hosts = self.hosts_by_pg_idx[src_if.sw_if_index] + for dst_if in self.flows[src_if]: + dst_hosts = self.hosts_by_pg_idx[dst_if.sw_if_index] + n_int = len(dst_hosts) + for i in range(0, n_int): + dst_host = dst_hosts[i] + src_host = random.choice(src_hosts) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + size = random.choice(packet_sizes) + self.extend_packet(p, size) + pkts.append(p) + self.logger.debug("Input stream created for port %s. Length: %u pkt(s)" + % (src_if.name, len(pkts))) + return pkts + + def verify_capture(self, pg_if, capture): + """ + Verify captured input packet stream for defined interface. + + :param object pg_if: Interface to verify captured packet stream for. + :param list capture: Captured packet stream. + """ + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = pg_if.sw_if_index + for packet in capture: + payload_info = self.payload_to_info(str(packet[Raw])) + try: + ip = packet[IP] + udp = packet[UDP] + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug("Got packet on port %s: src=%u (id=%u)" % + (pg_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip.src, saved_packet[IP].src) + self.assertEqual(ip.dst, saved_packet[IP].dst) + self.assertEqual(udp.sport, saved_packet[UDP].sport) + self.assertEqual(udp.dport, saved_packet[UDP].dport) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue( + remaining_packet is None, + "Port %u: Packet expected from source %u didn't arrive" % + (dst_sw_if_index, i.sw_if_index)) + + def run_verify_test(self): + """ + Create packet streams for all configured l2-pg interfaces, send all \ + prepared packet streams and verify that: + - all packets received correctly on all pg-l2 interfaces assigned + to cross-connects + - no packet received on all pg-l2 interfaces not assigned to + cross-connects + + :raise RuntimeError: if no packet captured on l2-pg interface assigned + to the cross-connect or if any packet is captured + on l2-pg interface not assigned to the + cross-connect. + """ + # Test + # Create incoming packet streams for packet-generator interfaces + for pg_if in self.pg_interfaces: + pkts = self.create_stream(pg_if, self.pg_if_packet_sizes) + pg_if.add_stream(pkts) + + # Enable packet capture and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify + # Verify outgoing packet streams per packet-generator interface + for pg_if in self.pg_interfaces: + if pg_if in self.pg_in_xc: + capture = pg_if.get_capture( + remark="interface is a cross-connect sink") + self.verify_capture(pg_if, capture) + elif pg_if in self.pg_not_in_xc: + pg_if.assert_nothing_captured( + remark="interface is not a cross-connect sink") + else: + raise Exception("Unexpected interface: %s" % pg_if.name) + + def test_l2xc_inst_01(self): + """ L2XC Multi-instance test 1 - create 10 cross-connects + """ + # Config 1 + # Create 10 cross-connects + self.create_xconnects(10) + + # Test 1 + self.run_verify_test() + + def test_l2xc_inst_02(self): + """ L2XC Multi-instance test 2 - delete 4 cross-connects + """ + # Config 2 + # Delete 4 cross-connects + self.delete_xconnects(4) + + # Test 2 + self.run_verify_test() + + def test_l2xc_inst_03(self): + """ L2BD Multi-instance 3 - add new 4 cross-connects + """ + # Config 3 + # Add new 4 cross-connects + self.create_xconnects(4, start=10) + + # Test 3 + self.run_verify_test() + + def test_l2xc_inst_04(self): + """ L2XC Multi-instance test 4 - delete 10 cross-connects + """ + # Config 4 + # Delete 10 cross-connects + self.delete_xconnects(10, start=4) + + # Test 4 + self.run_verify_test() + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_lb.py b/test/test_lb.py new file mode 100644 index 00000000..e5802d99 --- /dev/null +++ b/test/test_lb.py @@ -0,0 +1,216 @@ +import socket + +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether, GRE +from scapy.packet import Raw + +from framework import VppTestCase +from util import ppp + +""" TestLB is a subclass of VPPTestCase classes. + + TestLB class defines Load Balancer test cases for: + - IP4 to GRE4 encap + - IP4 to GRE6 encap + - IP6 to GRE4 encap + - IP6 to GRE6 encap + + As stated in comments below, GRE has issues with IPv6. + All test cases involving IPv6 are executed, but + received packets are not parsed and checked. + +""" + + +class TestLB(VppTestCase): + """ Load Balancer Test Case """ + + @classmethod + def setUpClass(cls): + super(TestLB, cls).setUpClass() + + cls.ass = range(5) + cls.packets = range(100) + + try: + cls.create_pg_interfaces(range(2)) + cls.interfaces = list(cls.pg_interfaces) + + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.config_ip6() + i.disable_ipv6_ra() + i.resolve_arp() + i.resolve_ndp() + dst4 = socket.inet_pton(socket.AF_INET, "10.0.0.0") + dst6 = socket.inet_pton(socket.AF_INET6, "2002::") + cls.vapi.ip_add_del_route(dst4, 24, cls.pg1.remote_ip4n) + cls.vapi.ip_add_del_route(dst6, 16, cls.pg1.remote_ip6n, is_ipv6=1) + cls.vapi.cli("lb conf ip4-src-address 39.40.41.42") + cls.vapi.cli("lb conf ip6-src-address 2004::1") + except Exception: + super(TestLB, cls).tearDownClass() + raise + + def tearDown(self): + super(TestLB, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show lb vip verbose")) + + def getIPv4Flow(self, id): + return (IP(dst="90.0.%u.%u" % (id / 255, id % 255), + src="40.0.%u.%u" % (id / 255, id % 255)) / + UDP(sport=10000 + id, dport=20000 + id)) + + def getIPv6Flow(self, id): + return (IPv6(dst="2001::%u" % (id), src="fd00:f00d:ffff::%u" % (id)) / + UDP(sport=10000 + id, dport=20000 + id)) + + def generatePackets(self, src_if, isv4): + self.reset_packet_infos() + pkts = [] + for pktid in self.packets: + info = self.create_packet_info(src_if, self.pg1) + payload = self.info_to_payload(info) + ip = self.getIPv4Flow(pktid) if isv4 else self.getIPv6Flow(pktid) + packet = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + ip / + Raw(payload)) + self.extend_packet(packet, 128) + info.data = packet.copy() + pkts.append(packet) + return pkts + + def checkInner(self, gre, isv4): + IPver = IP if isv4 else IPv6 + self.assertEqual(gre.proto, 0x0800 if isv4 else 0x86DD) + self.assertEqual(gre.flags, 0) + self.assertEqual(gre.version, 0) + inner = IPver(str(gre.payload)) + payload_info = self.payload_to_info(str(inner[Raw])) + self.info = self.packet_infos[payload_info.index] + self.assertEqual(payload_info.src, self.pg0.sw_if_index) + self.assertEqual(str(inner), str(self.info.data[IPver])) + + def checkCapture(self, gre4, isv4): + self.pg0.assert_nothing_captured() + out = self.pg1.get_capture(len(self.packets)) + + load = [0] * len(self.ass) + self.info = None + for p in out: + try: + asid = 0 + gre = None + if gre4: + ip = p[IP] + asid = int(ip.dst.split(".")[3]) + self.assertEqual(ip.version, 4) + self.assertEqual(ip.flags, 0) + self.assertEqual(ip.src, "39.40.41.42") + self.assertEqual(ip.dst, "10.0.0.%u" % asid) + self.assertEqual(ip.proto, 47) + self.assertEqual(len(ip.options), 0) + self.assertGreaterEqual(ip.ttl, 64) + gre = p[GRE] + else: + ip = p[IPv6] + asid = ip.dst.split(":") + asid = asid[len(asid) - 1] + asid = 0 if asid == "" else int(asid) + self.assertEqual(ip.version, 6) + self.assertEqual(ip.tc, 0) + self.assertEqual(ip.fl, 0) + self.assertEqual(ip.src, "2004::1") + self.assertEqual( + socket.inet_pton(socket.AF_INET6, ip.dst), + socket.inet_pton(socket.AF_INET6, "2002::%u" % asid) + ) + self.assertEqual(ip.nh, 47) + self.assertGreaterEqual(ip.hlim, 64) + # self.assertEqual(len(ip.options), 0) + gre = GRE(str(p[IPv6].payload)) + self.checkInner(gre, isv4) + load[asid] += 1 + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # This is just to roughly check that the balancing algorithm + # is not completly biased. + for asid in self.ass: + if load[asid] < len(self.packets) / (len(self.ass) * 2): + self.log( + "ASS is not balanced: load[%d] = %d" % (asid, load[asid])) + raise Exception("Load Balancer algorithm is biased") + + def test_lb_ip4_gre4(self): + """ Load Balancer IP4 GRE4 """ + try: + self.vapi.cli("lb vip 90.0.0.0/8 encap gre4") + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 10.0.0.%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=True)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.checkCapture(gre4=True, isv4=True) + + finally: + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 10.0.0.%u del" % (asid)) + self.vapi.cli("lb vip 90.0.0.0/8 encap gre4 del") + + def test_lb_ip6_gre4(self): + """ Load Balancer IP6 GRE4 """ + + try: + self.vapi.cli("lb vip 2001::/16 encap gre4") + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 10.0.0.%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=False)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.checkCapture(gre4=True, isv4=False) + finally: + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 10.0.0.%u del" % (asid)) + self.vapi.cli("lb vip 2001::/16 encap gre4 del") + + def test_lb_ip4_gre6(self): + """ Load Balancer IP4 GRE6 """ + try: + self.vapi.cli("lb vip 90.0.0.0/8 encap gre6") + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 2002::%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=True)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.checkCapture(gre4=False, isv4=True) + finally: + for asid in self.ass: + self.vapi.cli("lb as 90.0.0.0/8 2002::%u" % (asid)) + self.vapi.cli("lb vip 90.0.0.0/8 encap gre6 del") + + def test_lb_ip6_gre6(self): + """ Load Balancer IP6 GRE6 """ + try: + self.vapi.cli("lb vip 2001::/16 encap gre6") + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 2002::%u" % (asid)) + + self.pg0.add_stream(self.generatePackets(self.pg0, isv4=False)) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.checkCapture(gre4=False, isv4=False) + finally: + for asid in self.ass: + self.vapi.cli("lb as 2001::/16 2002::%u del" % (asid)) + self.vapi.cli("lb vip 2001::/16 encap gre6 del") diff --git a/test/test_lisp.py b/test/test_lisp.py new file mode 100644 index 00000000..cfe8e0af --- /dev/null +++ b/test/test_lisp.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +import unittest + +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP, Ether +from py_lispnetworking.lisp import LISP_GPE_Header + +from util import ppp, ForeignAddressFactory +from framework import VppTestCase, VppTestRunner +from lisp import * + + +class Driver(object): + + config_order = ['locator-sets', + 'locators', + 'local-mappings', + 'remote-mappings', + 'adjacencies'] + + """ Basic class for data driven testing """ + def __init__(self, test, test_cases): + self._test_cases = test_cases + self._test = test + + @property + def test_cases(self): + return self._test_cases + + @property + def test(self): + return self._test + + def create_packet(self, src_if, dst_if, deid, payload=''): + """ + Create IPv4 packet + + param: src_if + param: dst_if + """ + packet = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=deid) / + Raw(payload)) + return packet + + @abstractmethod + def run(self): + """ testing procedure """ + pass + + +class SimpleDriver(Driver): + """ Implements simple test procedure """ + def __init__(self, test, test_cases): + super(SimpleDriver, self).__init__(test, test_cases) + + def verify_capture(self, src_loc, dst_loc, capture): + """ + Verify captured packet + + :param src_loc: source locator address + :param dst_loc: destination locator address + :param capture: list of captured packets + """ + self.test.assertEqual(len(capture), 1, "Unexpected number of " + "packets! Expected 1 but {} received" + .format(len(capture))) + packet = capture[0] + try: + ip_hdr = packet[IP] + # assert the values match + self.test.assertEqual(ip_hdr.src, src_loc, "IP source address") + self.test.assertEqual(ip_hdr.dst, dst_loc, + "IP destination address") + gpe_hdr = packet[LISP_GPE_Header] + self.test.assertEqual(gpe_hdr.next_proto, 1, + "next_proto is not ipv4!") + ih = gpe_hdr[IP] + self.test.assertEqual(ih.src, self.test.pg0.remote_ip4, + "unexpected source EID!") + self.test.assertEqual(ih.dst, self.test.deid_ip4, + "unexpected dest EID!") + except: + self.test.logger.error(ppp("Unexpected or invalid packet:", + packet)) + raise + + def configure_tc(self, tc): + for config_item in self.config_order: + for vpp_object in tc[config_item]: + vpp_object.add_vpp_config() + + def run(self, dest): + """ Send traffic for each test case and verify that it + is encapsulated """ + for tc in enumerate(self.test_cases): + self.test.logger.info('Running {}'.format(tc[1]['name'])) + self.configure_tc(tc[1]) + + packet = self.create_packet(self.test.pg0, self.test.pg1, dest, + 'data') + self.test.pg0.add_stream(packet) + self.test.pg0.enable_capture() + self.test.pg1.enable_capture() + self.test.pg_start() + capture = self.test.pg1.get_capture(1) + self.verify_capture(self.test.pg1.local_ip4, + self.test.pg1.remote_ip4, capture) + self.test.pg0.assert_nothing_captured() + + +class TestLisp(VppTestCase): + """ Basic LISP test """ + + @classmethod + def setUpClass(cls): + super(TestLisp, cls).setUpClass() + cls.faf = ForeignAddressFactory() + cls.create_pg_interfaces(range(2)) # create pg0 and pg1 + for i in cls.pg_interfaces: + i.admin_up() # put the interface upsrc_if + i.config_ip4() # configure IPv4 address on the interface + i.resolve_arp() # resolve ARP, so that we know VPP MAC + + def setUp(self): + super(TestLisp, self).setUp() + self.vapi.lisp_enable_disable(is_enabled=1) + + def test_lisp_basic_encap(self): + """Test case for basic encapsulation""" + + self.deid_ip4_net = self.faf.net + self.deid_ip4 = self.faf.get_ip4() + self.seid_ip4 = '{}/{}'.format(self.pg0.local_ip4, 32) + self.rloc_ip4 = self.pg1.remote_ip4n + + test_cases = [ + { + 'name': 'basic ip4 over ip4', + 'locator-sets': [VppLispLocatorSet(self, 'ls-4o4')], + 'locators': [ + VppLispLocator(self, self.pg1.sw_if_index, 'ls-4o4') + ], + 'local-mappings': [ + VppLocalMapping(self, self.seid_ip4, 'ls-4o4') + ], + 'remote-mappings': [ + VppRemoteMapping(self, self.deid_ip4_net, + [{ + "is_ip4": 1, + "priority": 1, + "weight": 1, + "addr": self.rloc_ip4 + }]) + ], + 'adjacencies': [ + VppLispAdjacency(self, self.seid_ip4, self.deid_ip4_net) + ] + } + ] + self.test_driver = SimpleDriver(self, test_cases) + self.test_driver.run(self.deid_ip4) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_map.py b/test/test_map.py new file mode 100644 index 00000000..bbf4aec2 --- /dev/null +++ b/test/test_map.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python + +import unittest +import socket + +from framework import VppTestCase, VppTestRunner +from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto + +from scapy.layers.l2 import Ether, Raw +from scapy.layers.inet import IP, UDP, ICMP +from scapy.layers.inet6 import IPv6 + + +class TestMAP(VppTestCase): + """ MAP Test Case """ + + def setUp(self): + super(TestMAP, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(4)) + + # pg0 is 'inside' IPv4 + self.pg0.admin_up() + self.pg0.config_ip4() + self.pg0.resolve_arp() + + # pg1 is 'outside' IPv6 + self.pg1.admin_up() + self.pg1.config_ip6() + self.pg1.generate_remote_hosts(4) + self.pg1.configure_ipv6_neighbors() + + def tearDown(self): + super(TestMAP, self).tearDown() + for i in self.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.admin_down() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.assert_nothing_captured(remark=remark) + + def send_and_assert_encapped(self, tx, ip6_src, ip6_dst, dmac=None): + if not dmac: + dmac = self.pg1.remote_mac + + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + rx = rx[0] + + self.assertEqual(rx[Ether].dst, dmac) + self.assertEqual(rx[IP].src, tx[IP].src) + self.assertEqual(rx[IPv6].src, ip6_src) + self.assertEqual(rx[IPv6].dst, ip6_dst) + + def test_map_e(self): + """ MAP-E """ + + # + # Add a route to the MAP-BR + # + map_br_pfx = "2001::" + map_br_pfx_len = 64 + map_route = VppIpRoute(self, + map_br_pfx, + map_br_pfx_len, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + map_route.add_vpp_config() + + # + # Add a domain that maps from pg0 to pg1 + # + map_dst = socket.inet_pton(socket.AF_INET6, map_br_pfx) + map_src = "3001::1" + map_src_n = socket.inet_pton(socket.AF_INET6, map_src) + client_pfx = socket.inet_pton(socket.AF_INET, "192.168.0.0") + + self.vapi.map_add_domain(map_dst, + map_br_pfx_len, + map_src_n, + 128, + client_pfx, + 16) + + # + # Fire in a v4 packet that will be encapped to the BR + # + v4 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst='192.168.1.1') / + UDP(sport=20000, dport=10000) / + Raw('\xa5' * 100)) + + self.send_and_assert_encapped(v4, map_src, "2001::c0a8:0:0") + + # + # Fire in a V6 encapped packet. + # expect a decapped packet on the inside ip4 link + # + p = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IPv6(dst=map_src, src="2001::1") / + IP(dst=self.pg0.remote_ip4, src='192.168.1.1') / + UDP(sport=20000, dport=10000) / + Raw('\xa5' * 100)) + + self.pg1.add_stream(p) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + rx = rx[0] + + self.assertFalse(rx.haslayer(IPv6)) + self.assertEqual(rx[IP].src, p[IP].src) + self.assertEqual(rx[IP].dst, p[IP].dst) + + # + # Pre-resolve. No API for this!! + # + self.vapi.ppcli("map params pre-resolve ip6-nh 4001::1") + + self.send_and_assert_no_replies(self.pg0, v4, + "resovled via default route") + + # + # Add a route to 4001::1. Expect the encapped traffic to be + # sent via that routes next-hop + # + pre_res_route = VppIpRoute( + self, "4001::1", 128, + [VppRoutePath(self.pg1.remote_hosts[2].ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + pre_res_route.add_vpp_config() + + self.send_and_assert_encapped(v4, map_src, + "2001::c0a8:0:0", + dmac=self.pg1.remote_hosts[2].mac) + + # + # change the route to the pre-solved next-hop + # + pre_res_route.modify([VppRoutePath(self.pg1.remote_hosts[3].ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)]) + pre_res_route.add_vpp_config() + + self.send_and_assert_encapped(v4, map_src, + "2001::c0a8:0:0", + dmac=self.pg1.remote_hosts[3].mac) + + # + # cleanup. The test infra's object registry will ensure + # the route is really gone and thus that the unresolve worked. + # + pre_res_route.remove_vpp_config() + self.vapi.ppcli("map params pre-resolve del ip6-nh 4001::1") + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_mpls.py b/test/test_mpls.py new file mode 100644 index 00000000..460a32d1 --- /dev/null +++ b/test/test_mpls.py @@ -0,0 +1,1771 @@ +#!/usr/bin/env python + +import unittest +import socket + +from framework import VppTestCase, VppTestRunner +from vpp_ip_route import VppIpRoute, VppRoutePath, VppMplsRoute, \ + VppMplsIpBind, VppIpMRoute, VppMRoutePath, \ + MRouteItfFlags, MRouteEntryFlags, DpoProto, VppIpTable, VppMplsTable +from vpp_mpls_tunnel_interface import VppMPLSTunnelInterface + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP, ICMP +from scapy.layers.inet6 import IPv6 +from scapy.contrib.mpls import MPLS + + +def verify_filter(capture, sent): + if not len(capture) == len(sent): + # filter out any IPv6 RAs from the capture + for p in capture: + if p.haslayer(IPv6): + capture.remove(p) + return capture + + +def verify_mpls_stack(tst, rx, mpls_labels, ttl=255, num=0): + # the rx'd packet has the MPLS label popped + eth = rx[Ether] + tst.assertEqual(eth.type, 0x8847) + + rx_mpls = rx[MPLS] + + for ii in range(len(mpls_labels)): + tst.assertEqual(rx_mpls.label, mpls_labels[ii]) + tst.assertEqual(rx_mpls.cos, 0) + if ii == num: + tst.assertEqual(rx_mpls.ttl, ttl) + else: + tst.assertEqual(rx_mpls.ttl, 255) + if ii == len(mpls_labels) - 1: + tst.assertEqual(rx_mpls.s, 1) + else: + # not end of stack + tst.assertEqual(rx_mpls.s, 0) + # pop the label to expose the next + rx_mpls = rx_mpls[MPLS].payload + + +class TestMPLS(VppTestCase): + """ MPLS Test Case """ + + def setUp(self): + super(TestMPLS, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(4)) + + # setup both interfaces + # assign them different tables. + table_id = 0 + self.tables = [] + + tbl = VppMplsTable(self, 0) + tbl.add_vpp_config() + self.tables.append(tbl) + + for i in self.pg_interfaces: + i.admin_up() + + if table_id != 0: + tbl = VppIpTable(self, table_id) + tbl.add_vpp_config() + self.tables.append(tbl) + tbl = VppIpTable(self, table_id, is_ip6=1) + tbl.add_vpp_config() + self.tables.append(tbl) + + i.set_table_ip4(table_id) + i.set_table_ip6(table_id) + i.config_ip4() + i.resolve_arp() + i.config_ip6() + i.resolve_ndp() + i.enable_mpls() + table_id += 1 + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.ip6_disable() + i.set_table_ip4(0) + i.set_table_ip6(0) + i.disable_mpls() + i.admin_down() + super(TestMPLS, self).tearDown() + + # the default of 64 matches the IP packet TTL default + def create_stream_labelled_ip4( + self, + src_if, + mpls_labels, + mpls_ttl=255, + ping=0, + ip_itf=None, + dst_ip=None, + n=257): + self.reset_packet_infos() + pkts = [] + for i in range(0, n): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = Ether(dst=src_if.local_mac, src=src_if.remote_mac) + + for ii in range(len(mpls_labels)): + if ii == len(mpls_labels) - 1: + p = p / MPLS(label=mpls_labels[ii], ttl=mpls_ttl, s=1) + else: + p = p / MPLS(label=mpls_labels[ii], ttl=mpls_ttl, s=0) + if not ping: + if not dst_ip: + p = (p / IP(src=src_if.local_ip4, dst=src_if.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + else: + p = (p / IP(src=src_if.local_ip4, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + else: + p = (p / IP(src=ip_itf.remote_ip4, + dst=ip_itf.local_ip4) / + ICMP()) + + info.data = p.copy() + pkts.append(p) + return pkts + + def create_stream_ip4(self, src_if, dst_ip): + self.reset_packet_infos() + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def create_stream_labelled_ip6(self, src_if, mpls_label, mpls_ttl, + dst_ip=None): + if dst_ip is None: + dst_ip = src_if.remote_ip6 + self.reset_packet_infos() + pkts = [] + for i in range(0, 257): + info = self.create_packet_info(src_if, src_if) + payload = self.info_to_payload(info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + MPLS(label=mpls_label, ttl=mpls_ttl) / + IPv6(src=src_if.remote_ip6, dst=dst_ip) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + info.data = p.copy() + pkts.append(p) + return pkts + + def verify_capture_ip4(self, src_if, capture, sent, ping_resp=0): + try: + capture = verify_filter(capture, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + + # the rx'd packet has the MPLS label popped + eth = rx[Ether] + self.assertEqual(eth.type, 0x800) + + tx_ip = tx[IP] + rx_ip = rx[IP] + + if not ping_resp: + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + else: + self.assertEqual(rx_ip.src, tx_ip.dst) + self.assertEqual(rx_ip.dst, tx_ip.src) + + except: + raise + + def verify_capture_labelled_ip4(self, src_if, capture, sent, + mpls_labels): + try: + capture = verify_filter(capture, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + tx_ip = tx[IP] + rx_ip = rx[IP] + + # the MPLS TTL is copied from the IP + verify_mpls_stack(self, rx, mpls_labels, rx_ip.ttl, + len(mpls_labels) - 1) + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + raise + + def verify_capture_tunneled_ip4(self, src_if, capture, sent, mpls_labels, + ttl=255, top=None): + if top is None: + top = len(mpls_labels) - 1 + try: + capture = verify_filter(capture, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + tx_ip = tx[IP] + rx_ip = rx[IP] + + # the MPLS TTL is 255 since it enters a new tunnel + verify_mpls_stack(self, rx, mpls_labels, ttl, top) + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.ttl + 1, tx_ip.ttl) + + except: + raise + + def verify_capture_labelled(self, src_if, capture, sent, + mpls_labels, ttl=254, num=0): + try: + capture = verify_filter(capture, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + rx = capture[i] + verify_mpls_stack(self, rx, mpls_labels, ttl, num) + except: + raise + + def verify_capture_ip6(self, src_if, capture, sent): + try: + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + + # the rx'd packet has the MPLS label popped + eth = rx[Ether] + self.assertEqual(eth.type, 0x86DD) + + tx_ip = tx[IPv6] + rx_ip = rx[IPv6] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + # IP processing post pop has decremented the TTL + self.assertEqual(rx_ip.hlim + 1, tx_ip.hlim) + + except: + raise + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.assert_nothing_captured(remark=remark) + + def test_swap(self): + """ MPLS label swap tests """ + + # + # A simple MPLS xconnect - eos label in label out + # + route_32_eos = VppMplsRoute(self, 32, 1, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[33])]) + route_32_eos.add_vpp_config() + + # + # a stream that matches the route for 10.0.0.1 + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [32]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled(self.pg0, rx, tx, [33]) + + # + # A simple MPLS xconnect - non-eos label in label out + # + route_32_neos = VppMplsRoute(self, 32, 0, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[33])]) + route_32_neos.add_vpp_config() + + # + # a stream that matches the route for 10.0.0.1 + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [32, 99]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled(self.pg0, rx, tx, [33, 99]) + + # + # An MPLS xconnect - EOS label in IP out + # + route_33_eos = VppMplsRoute(self, 33, 1, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[])]) + route_33_eos.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [33]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_ip4(self.pg0, rx, tx) + + # + # An MPLS xconnect - non-EOS label in IP out - an invalid configuration + # so this traffic should be dropped. + # + route_33_neos = VppMplsRoute(self, 33, 0, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[])]) + route_33_neos.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [33, 99]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.assert_nothing_captured( + remark="MPLS non-EOS packets popped and forwarded") + + # + # A recursive EOS x-connect, which resolves through another x-connect + # + route_34_eos = VppMplsRoute(self, 34, 1, + [VppRoutePath("0.0.0.0", + 0xffffffff, + nh_via_label=32, + labels=[44, 45])]) + route_34_eos.add_vpp_config() + + tx = self.create_stream_labelled_ip4(self.pg0, [34]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled(self.pg0, rx, tx, [33, 44, 45], num=2) + + # + # A recursive non-EOS x-connect, which resolves through another + # x-connect + # + route_34_neos = VppMplsRoute(self, 34, 0, + [VppRoutePath("0.0.0.0", + 0xffffffff, + nh_via_label=32, + labels=[44, 46])]) + route_34_neos.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [34, 99]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + # it's the 2nd (counting from 0) label in the stack that is swapped + self.verify_capture_labelled(self.pg0, rx, tx, [33, 44, 46, 99], num=2) + + # + # an recursive IP route that resolves through the recursive non-eos + # x-connect + # + ip_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + [VppRoutePath("0.0.0.0", + 0xffffffff, + nh_via_label=34, + labels=[55])]) + ip_10_0_0_1.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4(self.pg0, rx, tx, [33, 44, 46, 55]) + + ip_10_0_0_1.remove_vpp_config() + route_34_neos.remove_vpp_config() + route_34_eos.remove_vpp_config() + route_33_neos.remove_vpp_config() + route_33_eos.remove_vpp_config() + route_32_neos.remove_vpp_config() + route_32_eos.remove_vpp_config() + + def test_bind(self): + """ MPLS Local Label Binding test """ + + # + # Add a non-recursive route with a single out label + # + route_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[45])]) + route_10_0_0_1.add_vpp_config() + + # bind a local label to the route + binding = VppMplsIpBind(self, 44, "10.0.0.1", 32) + binding.add_vpp_config() + + # non-EOS stream + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [44, 99]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled(self.pg0, rx, tx, [45, 99]) + + # EOS stream + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [44]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled(self.pg0, rx, tx, [45]) + + # IP stream + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4(self.pg0, rx, tx, [45]) + + # + # cleanup + # + binding.remove_vpp_config() + route_10_0_0_1.remove_vpp_config() + + def test_imposition(self): + """ MPLS label imposition test """ + + # + # Add a non-recursive route with a single out label + # + route_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[32])]) + route_10_0_0_1.add_vpp_config() + + # + # a stream that matches the route for 10.0.0.1 + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4(self.pg0, rx, tx, [32]) + + # + # Add a non-recursive route with a 3 out labels + # + route_10_0_0_2 = VppIpRoute(self, "10.0.0.2", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[32, 33, 34])]) + route_10_0_0_2.add_vpp_config() + + # + # a stream that matches the route for 10.0.0.1 + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.2") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4(self.pg0, rx, tx, [32, 33, 34]) + + # + # add a recursive path, with output label, via the 1 label route + # + route_11_0_0_1 = VppIpRoute(self, "11.0.0.1", 32, + [VppRoutePath("10.0.0.1", + 0xffffffff, + labels=[44])]) + route_11_0_0_1.add_vpp_config() + + # + # a stream that matches the route for 11.0.0.1, should pick up + # the label stack for 11.0.0.1 and 10.0.0.1 + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "11.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4(self.pg0, rx, tx, [32, 44]) + + # + # add a recursive path, with 2 labels, via the 3 label route + # + route_11_0_0_2 = VppIpRoute(self, "11.0.0.2", 32, + [VppRoutePath("10.0.0.2", + 0xffffffff, + labels=[44, 45])]) + route_11_0_0_2.add_vpp_config() + + # + # a stream that matches the route for 11.0.0.1, should pick up + # the label stack for 11.0.0.1 and 10.0.0.1 + # + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "11.0.0.2") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_labelled_ip4( + self.pg0, rx, tx, [32, 33, 34, 44, 45]) + + # + # cleanup + # + route_11_0_0_2.remove_vpp_config() + route_11_0_0_1.remove_vpp_config() + route_10_0_0_2.remove_vpp_config() + route_10_0_0_1.remove_vpp_config() + + def test_tunnel(self): + """ MPLS Tunnel Tests """ + + # + # Create a tunnel with a single out label + # + mpls_tun = VppMPLSTunnelInterface(self, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[44, 46])]) + mpls_tun.add_vpp_config() + mpls_tun.admin_up() + + # + # add an unlabelled route through the new tunnel + # + route_10_0_0_3 = VppIpRoute(self, "10.0.0.3", 32, + [VppRoutePath("0.0.0.0", + mpls_tun._sw_if_index)]) + route_10_0_0_3.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.3") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [44, 46]) + + # + # add a labelled route through the new tunnel + # + route_10_0_0_4 = VppIpRoute(self, "10.0.0.4", 32, + [VppRoutePath("0.0.0.0", + mpls_tun._sw_if_index, + labels=[33])]) + route_10_0_0_4.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.4") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [44, 46, 33], + ttl=63, top=2) + + def test_v4_exp_null(self): + """ MPLS V4 Explicit NULL test """ + + # + # The first test case has an MPLS TTL of 0 + # all packet should be dropped + # + tx = self.create_stream_labelled_ip4(self.pg0, [0], 0) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg0.assert_nothing_captured(remark="MPLS TTL=0 packets forwarded") + + # + # a stream with a non-zero MPLS TTL + # PG0 is in the default table + # + tx = self.create_stream_labelled_ip4(self.pg0, [0]) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_ip4(self.pg0, rx, tx) + + # + # a stream with a non-zero MPLS TTL + # PG1 is in table 1 + # we are ensuring the post-pop lookup occurs in the VRF table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg1, [0]) + self.pg1.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture() + self.verify_capture_ip4(self.pg0, rx, tx) + + def test_v6_exp_null(self): + """ MPLS V6 Explicit NULL test """ + + # + # a stream with a non-zero MPLS TTL + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip6(self.pg0, 2, 2) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_ip6(self.pg0, rx, tx) + + # + # a stream with a non-zero MPLS TTL + # PG1 is in table 1 + # we are ensuring the post-pop lookup occurs in the VRF table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip6(self.pg1, 2, 2) + self.pg1.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture() + self.verify_capture_ip6(self.pg0, rx, tx) + + def test_deag(self): + """ MPLS Deagg """ + + # + # A de-agg route - next-hop lookup in default table + # + route_34_eos = VppMplsRoute(self, 34, 1, + [VppRoutePath("0.0.0.0", + 0xffffffff, + nh_table_id=0)]) + route_34_eos.add_vpp_config() + + # + # ping an interface in the default table + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [34], ping=1, + ip_itf=self.pg0) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture() + self.verify_capture_ip4(self.pg0, rx, tx, ping_resp=1) + + # + # A de-agg route - next-hop lookup in non-default table + # + route_35_eos = VppMplsRoute(self, 35, 1, + [VppRoutePath("0.0.0.0", + 0xffffffff, + nh_table_id=1)]) + route_35_eos.add_vpp_config() + + # + # ping an interface in the non-default table + # PG0 is in the default table. packet arrive labelled in the + # default table and egress unlabelled in the non-default + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4( + self.pg0, [35], ping=1, ip_itf=self.pg1) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + packet_count = self.get_packet_count_for_if_idx(self.pg0.sw_if_index) + rx = self.pg1.get_capture(packet_count) + self.verify_capture_ip4(self.pg1, rx, tx, ping_resp=1) + + # + # Double pop + # + route_36_neos = VppMplsRoute(self, 36, 0, + [VppRoutePath("0.0.0.0", + 0xffffffff)]) + route_36_neos.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [36, 35], + ping=1, ip_itf=self.pg1) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(len(tx)) + self.verify_capture_ip4(self.pg1, rx, tx, ping_resp=1) + + route_36_neos.remove_vpp_config() + route_35_eos.remove_vpp_config() + route_34_eos.remove_vpp_config() + + def test_interface_rx(self): + """ MPLS Interface Receive """ + + # + # Add a non-recursive route that will forward the traffic + # post-interface-rx + # + route_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + table_id=1, + paths=[VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_10_0_0_1.add_vpp_config() + + # + # An interface receive label that maps traffic to RX on interface + # pg1 + # by injecting the packet in on pg0, which is in table 0 + # doing an interface-rx on pg1 and matching a route in table 1 + # if the packet egresses, then we must have swapped to pg1 + # so as to have matched the route in table 1 + # + route_34_eos = VppMplsRoute(self, 34, 1, + [VppRoutePath("0.0.0.0", + self.pg1.sw_if_index, + is_interface_rx=1)]) + route_34_eos.add_vpp_config() + + # + # ping an interface in the default table + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [34], n=257, + dst_ip="10.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(257) + self.verify_capture_ip4(self.pg1, rx, tx) + + def test_mcast_mid_point(self): + """ MPLS Multicast Mid Point """ + + # + # Add a non-recursive route that will forward the traffic + # post-interface-rx + # + route_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + table_id=1, + paths=[VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_10_0_0_1.add_vpp_config() + + # + # Add a mcast entry that replicate to pg2 and pg3 + # and replicate to a interface-rx (like a bud node would) + # + route_3400_eos = VppMplsRoute(self, 3400, 1, + [VppRoutePath(self.pg2.remote_ip4, + self.pg2.sw_if_index, + labels=[3401]), + VppRoutePath(self.pg3.remote_ip4, + self.pg3.sw_if_index, + labels=[3402]), + VppRoutePath("0.0.0.0", + self.pg1.sw_if_index, + is_interface_rx=1)], + is_multicast=1) + route_3400_eos.add_vpp_config() + + # + # ping an interface in the default table + # PG0 is in the default table + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [3400], n=257, + dst_ip="10.0.0.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(257) + self.verify_capture_ip4(self.pg1, rx, tx) + + rx = self.pg2.get_capture(257) + self.verify_capture_labelled(self.pg2, rx, tx, [3401]) + rx = self.pg3.get_capture(257) + self.verify_capture_labelled(self.pg3, rx, tx, [3402]) + + def test_mcast_head(self): + """ MPLS Multicast Head-end """ + + # + # Create a multicast tunnel with two replications + # + mpls_tun = VppMPLSTunnelInterface(self, + [VppRoutePath(self.pg2.remote_ip4, + self.pg2.sw_if_index, + labels=[42]), + VppRoutePath(self.pg3.remote_ip4, + self.pg3.sw_if_index, + labels=[43])], + is_multicast=1) + mpls_tun.add_vpp_config() + mpls_tun.admin_up() + + # + # add an unlabelled route through the new tunnel + # + route_10_0_0_3 = VppIpRoute(self, "10.0.0.3", 32, + [VppRoutePath("0.0.0.0", + mpls_tun._sw_if_index)]) + route_10_0_0_3.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "10.0.0.3") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(257) + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [42]) + rx = self.pg3.get_capture(257) + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [43]) + + # + # An an IP multicast route via the tunnel + # A (*,G). + # one accepting interface, pg0, 1 forwarding interface via the tunnel + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + [VppMRoutePath(self.pg0.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_ACCEPT), + VppMRoutePath(mpls_tun._sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_232_1_1_1.add_vpp_config() + + self.vapi.cli("clear trace") + tx = self.create_stream_ip4(self.pg0, "232.1.1.1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(257) + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [42]) + rx = self.pg3.get_capture(257) + self.verify_capture_tunneled_ip4(self.pg0, rx, tx, [43]) + + def test_mcast_ip4_tail(self): + """ MPLS IPv4 Multicast Tail """ + + # + # Add a multicast route that will forward the traffic + # post-disposition + # + route_232_1_1_1 = VppIpMRoute( + self, + "0.0.0.0", + "232.1.1.1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + table_id=1, + paths=[VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)]) + route_232_1_1_1.add_vpp_config() + + # + # An interface receive label that maps traffic to RX on interface + # pg1 + # by injecting the packet in on pg0, which is in table 0 + # doing an rpf-id and matching a route in table 1 + # if the packet egresses, then we must have matched the route in + # table 1 + # + route_34_eos = VppMplsRoute(self, 34, 1, + [VppRoutePath("0.0.0.0", + self.pg1.sw_if_index, + nh_table_id=1, + rpf_id=55)], + is_multicast=1) + + route_34_eos.add_vpp_config() + + # + # Drop due to interface lookup miss + # + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [34], + dst_ip="232.1.1.1", n=1) + self.send_and_assert_no_replies(self.pg0, tx, "RPF-ID drop none") + + # + # set the RPF-ID of the enrtry to match the input packet's + # + route_232_1_1_1.update_rpf_id(55) + + self.vapi.cli("clear trace") + tx = self.create_stream_labelled_ip4(self.pg0, [34], + dst_ip="232.1.1.1", n=257) + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(257) + self.verify_capture_ip4(self.pg1, rx, tx) + + # + # set the RPF-ID of the enrtry to not match the input packet's + # + route_232_1_1_1.update_rpf_id(56) + tx = self.create_stream_labelled_ip4(self.pg0, [34], + dst_ip="232.1.1.1") + self.send_and_assert_no_replies(self.pg0, tx, "RPF-ID drop 56") + + def test_mcast_ip6_tail(self): + """ MPLS IPv6 Multicast Tail """ + + # + # Add a multicast route that will forward the traffic + # post-disposition + # + route_ff = VppIpMRoute( + self, + "::", + "ff01::1", 32, + MRouteEntryFlags.MFIB_ENTRY_FLAG_NONE, + table_id=1, + paths=[VppMRoutePath(self.pg1.sw_if_index, + MRouteItfFlags.MFIB_ITF_FLAG_FORWARD)], + is_ip6=1) + route_ff.add_vpp_config() + + # + # An interface receive label that maps traffic to RX on interface + # pg1 + # by injecting the packet in on pg0, which is in table 0 + # doing an rpf-id and matching a route in table 1 + # if the packet egresses, then we must have matched the route in + # table 1 + # + route_34_eos = VppMplsRoute( + self, 34, 1, + [VppRoutePath("::", + self.pg1.sw_if_index, + nh_table_id=1, + rpf_id=55, + proto=DpoProto.DPO_PROTO_IP6)], + is_multicast=1) + + route_34_eos.add_vpp_config() + + # + # Drop due to interface lookup miss + # + tx = self.create_stream_labelled_ip6(self.pg0, [34], 255, + dst_ip="ff01::1") + + # + # set the RPF-ID of the enrtry to match the input packet's + # + route_ff.update_rpf_id(55) + + tx = self.create_stream_labelled_ip6(self.pg0, [34], 255, + dst_ip="ff01::1") + self.pg0.add_stream(tx) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(257) + self.verify_capture_ip6(self.pg1, rx, tx) + + # + # set the RPF-ID of the enrtry to not match the input packet's + # + route_ff.update_rpf_id(56) + tx = self.create_stream_labelled_ip6(self.pg0, [34], 225, + dst_ip="ff01::1") + self.send_and_assert_no_replies(self.pg0, tx, "RPF-ID drop 56") + + +class TestMPLSDisabled(VppTestCase): + """ MPLS disabled """ + + def setUp(self): + super(TestMPLSDisabled, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(2)) + + self.tbl = VppMplsTable(self, 0) + self.tbl.add_vpp_config() + + # PG0 is MPLS enalbed + self.pg0.admin_up() + self.pg0.config_ip4() + self.pg0.resolve_arp() + self.pg0.enable_mpls() + + # PG 1 is not MPLS enabled + self.pg1.admin_up() + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + self.pg0.disable_mpls() + super(TestMPLSDisabled, self).tearDown() + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + for i in self.pg_interfaces: + i.get_capture(0) + i.assert_nothing_captured(remark=remark) + + def test_mpls_disabled(self): + """ MPLS Disabled """ + + tx = (Ether(src=self.pg1.remote_mac, + dst=self.pg1.local_mac) / + MPLS(label=32, ttl=64) / + IPv6(src="2001::1", dst=self.pg0.remote_ip6) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # A simple MPLS xconnect - eos label in label out + # + route_32_eos = VppMplsRoute(self, 32, 1, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[33])]) + route_32_eos.add_vpp_config() + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, tx, "MPLS disabled") + + # + # MPLS enable PG1 + # + self.pg1.enable_mpls() + + # + # Now we get packets through + # + self.pg1.add_stream(tx) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + + # + # Disable PG1 + # + self.pg1.disable_mpls() + + # + # PG1 does not forward IP traffic + # + self.send_and_assert_no_replies(self.pg1, tx, "IPv6 disabled") + self.send_and_assert_no_replies(self.pg1, tx, "IPv6 disabled") + + +class TestMPLSPIC(VppTestCase): + """ MPLS PIC edge convergence """ + + def setUp(self): + super(TestMPLSPIC, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(4)) + + mpls_tbl = VppMplsTable(self, 0) + mpls_tbl.add_vpp_config() + tbl4 = VppIpTable(self, 1) + tbl4.add_vpp_config() + tbl6 = VppIpTable(self, 1, is_ip6=1) + tbl6.add_vpp_config() + + # core links + self.pg0.admin_up() + self.pg0.config_ip4() + self.pg0.resolve_arp() + self.pg0.enable_mpls() + self.pg1.admin_up() + self.pg1.config_ip4() + self.pg1.resolve_arp() + self.pg1.enable_mpls() + + # VRF (customer facing) link + self.pg2.admin_up() + self.pg2.set_table_ip4(1) + self.pg2.config_ip4() + self.pg2.resolve_arp() + self.pg2.set_table_ip6(1) + self.pg2.config_ip6() + self.pg2.resolve_ndp() + self.pg3.admin_up() + self.pg3.set_table_ip4(1) + self.pg3.config_ip4() + self.pg3.resolve_arp() + self.pg3.set_table_ip6(1) + self.pg3.config_ip6() + self.pg3.resolve_ndp() + + def tearDown(self): + self.pg0.disable_mpls() + self.pg1.disable_mpls() + for i in self.pg_interfaces: + i.unconfig_ip4() + i.unconfig_ip6() + i.set_table_ip4(0) + i.set_table_ip6(0) + i.admin_down() + super(TestMPLSPIC, self).tearDown() + + def test_mpls_ibgp_pic(self): + """ MPLS iBGP PIC edge convergence + + 1) setup many iBGP VPN routes via a pair of iBGP peers. + 2) Check EMCP forwarding to these peers + 3) withdraw the IGP route to one of these peers. + 4) check forwarding continues to the remaining peer + """ + + # + # IGP+LDP core routes + # + core_10_0_0_45 = VppIpRoute(self, "10.0.0.45", 32, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[45])]) + core_10_0_0_45.add_vpp_config() + + core_10_0_0_46 = VppIpRoute(self, "10.0.0.46", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index, + labels=[46])]) + core_10_0_0_46.add_vpp_config() + + # + # Lot's of VPN routes. We need more the 64 so VPP will build + # the fast convergence indirection + # + vpn_routes = [] + pkts = [] + for ii in range(64): + dst = "192.168.1.%d" % ii + vpn_routes.append(VppIpRoute(self, dst, 32, + [VppRoutePath("10.0.0.45", + 0xffffffff, + labels=[145], + is_resolve_host=1), + VppRoutePath("10.0.0.46", + 0xffffffff, + labels=[146], + is_resolve_host=1)], + table_id=1)) + vpn_routes[ii].add_vpp_config() + + pkts.append(Ether(dst=self.pg2.local_mac, + src=self.pg2.remote_mac) / + IP(src=self.pg2.remote_ip4, dst=dst) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # Send the packet stream (one pkt to each VPN route) + # - expect a 50-50 split of the traffic + # + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0._get_capture(1) + rx1 = self.pg1._get_capture(1) + + # not testig the LB hashing algorithm so we're not concerned + # with the split ratio, just as long as neither is 0 + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + # + # use a test CLI command to stop the FIB walk process, this + # will prevent the FIB converging the VPN routes and thus allow + # us to probe the interim (psot-fail, pre-converge) state + # + self.vapi.ppcli("test fib-walk-process disable") + + # + # Withdraw one of the IGP routes + # + core_10_0_0_46.remove_vpp_config() + + # + # now all packets should be forwarded through the remaining peer + # + self.vapi.ppcli("clear trace") + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0.get_capture(len(pkts)) + + # + # enable the FIB walk process to converge the FIB + # + self.vapi.ppcli("test fib-walk-process enable") + + # + # packets should still be forwarded through the remaining peer + # + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0.get_capture(64) + + # + # Add the IGP route back and we return to load-balancing + # + core_10_0_0_46.add_vpp_config() + + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0._get_capture(1) + rx1 = self.pg1._get_capture(1) + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + def test_mpls_ebgp_pic(self): + """ MPLS eBGP PIC edge convergence + + 1) setup many eBGP VPN routes via a pair of eBGP peers + 2) Check EMCP forwarding to these peers + 3) withdraw one eBGP path - expect LB across remaining eBGP + """ + + # + # Lot's of VPN routes. We need more the 64 so VPP will build + # the fast convergence indirection + # + vpn_routes = [] + vpn_bindings = [] + pkts = [] + for ii in range(64): + dst = "192.168.1.%d" % ii + local_label = 1600 + ii + vpn_routes.append(VppIpRoute(self, dst, 32, + [VppRoutePath(self.pg2.remote_ip4, + 0xffffffff, + nh_table_id=1, + is_resolve_attached=1), + VppRoutePath(self.pg3.remote_ip4, + 0xffffffff, + nh_table_id=1, + is_resolve_attached=1)], + table_id=1)) + vpn_routes[ii].add_vpp_config() + + vpn_bindings.append(VppMplsIpBind(self, local_label, dst, 32, + ip_table_id=1)) + vpn_bindings[ii].add_vpp_config() + + pkts.append(Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + MPLS(label=local_label, ttl=64) / + IP(src=self.pg0.remote_ip4, dst=dst) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg2._get_capture(1) + rx1 = self.pg3._get_capture(1) + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + # + # use a test CLI command to stop the FIB walk process, this + # will prevent the FIB converging the VPN routes and thus allow + # us to probe the interim (psot-fail, pre-converge) state + # + self.vapi.ppcli("test fib-walk-process disable") + + # + # withdraw the connected prefix on the interface. + # + self.pg2.unconfig_ip4() + + # + # now all packets should be forwarded through the remaining peer + # + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg3.get_capture(len(pkts)) + + # + # enable the FIB walk process to converge the FIB + # + self.vapi.ppcli("test fib-walk-process enable") + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg3.get_capture(len(pkts)) + + # + # put the connecteds back + # + self.pg2.config_ip4() + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg2._get_capture(1) + rx1 = self.pg3._get_capture(1) + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + def test_mpls_v6_ebgp_pic(self): + """ MPLSv6 eBGP PIC edge convergence + + 1) setup many eBGP VPNv6 routes via a pair of eBGP peers + 2) Check EMCP forwarding to these peers + 3) withdraw one eBGP path - expect LB across remaining eBGP + """ + + # + # Lot's of VPN routes. We need more the 64 so VPP will build + # the fast convergence indirection + # + vpn_routes = [] + vpn_bindings = [] + pkts = [] + for ii in range(64): + dst = "3000::%d" % ii + local_label = 1600 + ii + vpn_routes.append(VppIpRoute( + self, dst, 128, + [VppRoutePath(self.pg2.remote_ip6, + 0xffffffff, + nh_table_id=1, + is_resolve_attached=1, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg3.remote_ip6, + 0xffffffff, + nh_table_id=1, + proto=DpoProto.DPO_PROTO_IP6, + is_resolve_attached=1)], + table_id=1, + is_ip6=1)) + vpn_routes[ii].add_vpp_config() + + vpn_bindings.append(VppMplsIpBind(self, local_label, dst, 128, + ip_table_id=1, + is_ip6=1)) + vpn_bindings[ii].add_vpp_config() + + pkts.append(Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + MPLS(label=local_label, ttl=64) / + IPv6(src=self.pg0.remote_ip6, dst=dst) / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg2._get_capture(1) + rx1 = self.pg3._get_capture(1) + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + # + # use a test CLI command to stop the FIB walk process, this + # will prevent the FIB converging the VPN routes and thus allow + # us to probe the interim (psot-fail, pre-converge) state + # + self.vapi.ppcli("test fib-walk-process disable") + + # + # withdraw the connected prefix on the interface. + # and shutdown the interface so the ND cache is flushed. + # + self.pg2.unconfig_ip6() + self.pg2.admin_down() + + # + # now all packets should be forwarded through the remaining peer + # + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg3.get_capture(len(pkts)) + + # + # enable the FIB walk process to converge the FIB + # + self.vapi.ppcli("test fib-walk-process enable") + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg3.get_capture(len(pkts)) + + # + # put the connecteds back + # + self.pg2.admin_up() + self.pg2.config_ip6() + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg2._get_capture(1) + rx1 = self.pg3._get_capture(1) + self.assertNotEqual(0, len(rx0)) + self.assertNotEqual(0, len(rx1)) + + +class TestMPLSL2(VppTestCase): + """ MPLS-L2 """ + + def setUp(self): + super(TestMPLSL2, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(2)) + + # create the default MPLS table + self.tables = [] + tbl = VppMplsTable(self, 0) + tbl.add_vpp_config() + self.tables.append(tbl) + + # use pg0 as the core facing interface + self.pg0.admin_up() + self.pg0.config_ip4() + self.pg0.resolve_arp() + self.pg0.enable_mpls() + + # use the other 2 for customer facing L2 links + for i in self.pg_interfaces[1:]: + i.admin_up() + + def tearDown(self): + for i in self.pg_interfaces[1:]: + i.admin_down() + + self.pg0.disable_mpls() + self.pg0.unconfig_ip4() + self.pg0.admin_down() + super(TestMPLSL2, self).tearDown() + + def verify_capture_tunneled_ethernet(self, capture, sent, mpls_labels, + ttl=255, top=None): + if top is None: + top = len(mpls_labels) - 1 + + capture = verify_filter(capture, sent) + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + tx = sent[i] + rx = capture[i] + + # the MPLS TTL is 255 since it enters a new tunnel + verify_mpls_stack(self, rx, mpls_labels, ttl, top) + + tx_eth = tx[Ether] + rx_eth = Ether(str(rx[MPLS].payload)) + + self.assertEqual(rx_eth.src, tx_eth.src) + self.assertEqual(rx_eth.dst, tx_eth.dst) + + def test_vpws(self): + """ Virtual Private Wire Service """ + + # + # Create an MPLS tunnel that pushes 1 label + # + mpls_tun_1 = VppMPLSTunnelInterface(self, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[42])], + is_l2=1) + mpls_tun_1.add_vpp_config() + mpls_tun_1.admin_up() + + # + # Create a label entry to for 55 that does L2 input to the tunnel + # + route_55_eos = VppMplsRoute( + self, 55, 1, + [VppRoutePath("0.0.0.0", + mpls_tun_1.sw_if_index, + is_interface_rx=1, + proto=DpoProto.DPO_PROTO_ETHERNET)]) + route_55_eos.add_vpp_config() + + # + # Cross-connect the tunnel with one of the customers L2 interfaces + # + self.vapi.sw_interface_set_l2_xconnect(self.pg1.sw_if_index, + mpls_tun_1.sw_if_index, + enable=1) + self.vapi.sw_interface_set_l2_xconnect(mpls_tun_1.sw_if_index, + self.pg1.sw_if_index, + enable=1) + + # + # inject a packet from the core + # + pcore = (Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + MPLS(label=55, ttl=64) / + Ether(dst="00:00:de:ad:ba:be", + src="00:00:de:ad:be:ef") / + IP(src="10.10.10.10", dst="11.11.11.11") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(pcore * 65) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg1.get_capture(65) + tx = pcore[MPLS].payload + + self.assertEqual(rx0[0][Ether].dst, tx[Ether].dst) + self.assertEqual(rx0[0][Ether].src, tx[Ether].src) + + # + # Inject a packet from the custoer/L2 side + # + self.pg1.add_stream(tx * 65) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0.get_capture(65) + + self.verify_capture_tunneled_ethernet(rx0, tx*65, [42]) + + def test_vpls(self): + """ Virtual Private LAN Service """ + # + # Create an L2 MPLS tunnel + # + mpls_tun = VppMPLSTunnelInterface(self, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + labels=[42])], + is_l2=1) + mpls_tun.add_vpp_config() + mpls_tun.admin_up() + + # + # Create a label entry to for 55 that does L2 input to the tunnel + # + route_55_eos = VppMplsRoute( + self, 55, 1, + [VppRoutePath("0.0.0.0", + mpls_tun.sw_if_index, + is_interface_rx=1, + proto=DpoProto.DPO_PROTO_ETHERNET)]) + route_55_eos.add_vpp_config() + + # + # add to tunnel to the customers bridge-domain + # + self.vapi.sw_interface_set_l2_bridge(mpls_tun.sw_if_index, + bd_id=1) + self.vapi.sw_interface_set_l2_bridge(self.pg1.sw_if_index, + bd_id=1) + + # + # Packet from the customer interface and from the core + # + p_cust = (Ether(dst="00:00:de:ad:ba:be", + src="00:00:de:ad:be:ef") / + IP(src="10.10.10.10", dst="11.11.11.11") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + p_core = (Ether(src="00:00:de:ad:ba:be", + dst="00:00:de:ad:be:ef") / + IP(dst="10.10.10.10", src="11.11.11.11") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + # + # The BD is learning, so send in one of each packet to learn + # + p_core_encap = (Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + MPLS(label=55, ttl=64) / + p_core) + + self.pg1.add_stream(p_cust) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.add_stream(p_core_encap) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # we've learnt this so expect it be be forwarded + rx0 = self.pg1.get_capture(1) + + self.assertEqual(rx0[0][Ether].dst, p_core[Ether].dst) + self.assertEqual(rx0[0][Ether].src, p_core[Ether].src) + + # + # now a stream in each direction + # + self.pg1.add_stream(p_cust * 65) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx0 = self.pg0.get_capture(65) + + self.verify_capture_tunneled_ethernet(rx0, p_cust*65, [42]) + + # + # remove interfaces from customers bridge-domain + # + self.vapi.sw_interface_set_l2_bridge(mpls_tun.sw_if_index, + bd_id=1, + enable=0) + self.vapi.sw_interface_set_l2_bridge(self.pg1.sw_if_index, + bd_id=1, + enable=0) + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_nat.py b/test/test_nat.py new file mode 100644 index 00000000..44758906 --- /dev/null +++ b/test/test_nat.py @@ -0,0 +1,3846 @@ +#!/usr/bin/env python + +import socket +import unittest +import struct + +from framework import VppTestCase, VppTestRunner, running_extended_tests +from scapy.layers.inet import IP, TCP, UDP, ICMP +from scapy.layers.inet import IPerror, TCPerror, UDPerror, ICMPerror +from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest, ICMPv6EchoReply +from scapy.layers.inet6 import ICMPv6DestUnreach, IPerror6 +from scapy.layers.l2 import Ether, ARP, GRE +from scapy.data import IP_PROTOS +from scapy.packet import bind_layers +from util import ppp +from ipfix import IPFIX, Set, Template, Data, IPFIXDecoder +from time import sleep +from util import ip4_range +from util import mactobinary + + +class MethodHolder(VppTestCase): + """ NAT create capture and verify method holder """ + + @classmethod + def setUpClass(cls): + super(MethodHolder, cls).setUpClass() + + def tearDown(self): + super(MethodHolder, self).tearDown() + + def check_ip_checksum(self, pkt): + """ + Check IP checksum of the packet + + :param pkt: Packet to check IP checksum + """ + new = pkt.__class__(str(pkt)) + del new['IP'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['IP'].chksum, pkt['IP'].chksum) + + def check_tcp_checksum(self, pkt): + """ + Check TCP checksum in IP packet + + :param pkt: Packet to check TCP checksum + """ + new = pkt.__class__(str(pkt)) + del new['TCP'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['TCP'].chksum, pkt['TCP'].chksum) + + def check_udp_checksum(self, pkt): + """ + Check UDP checksum in IP packet + + :param pkt: Packet to check UDP checksum + """ + new = pkt.__class__(str(pkt)) + del new['UDP'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['UDP'].chksum, pkt['UDP'].chksum) + + def check_icmp_errror_embedded(self, pkt): + """ + Check ICMP error embeded packet checksum + + :param pkt: Packet to check ICMP error embeded packet checksum + """ + if pkt.haslayer(IPerror): + new = pkt.__class__(str(pkt)) + del new['IPerror'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['IPerror'].chksum, pkt['IPerror'].chksum) + + if pkt.haslayer(TCPerror): + new = pkt.__class__(str(pkt)) + del new['TCPerror'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['TCPerror'].chksum, pkt['TCPerror'].chksum) + + if pkt.haslayer(UDPerror): + if pkt['UDPerror'].chksum != 0: + new = pkt.__class__(str(pkt)) + del new['UDPerror'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['UDPerror'].chksum, + pkt['UDPerror'].chksum) + + if pkt.haslayer(ICMPerror): + del new['ICMPerror'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['ICMPerror'].chksum, pkt['ICMPerror'].chksum) + + def check_icmp_checksum(self, pkt): + """ + Check ICMP checksum in IPv4 packet + + :param pkt: Packet to check ICMP checksum + """ + new = pkt.__class__(str(pkt)) + del new['ICMP'].chksum + new = new.__class__(str(new)) + self.assertEqual(new['ICMP'].chksum, pkt['ICMP'].chksum) + if pkt.haslayer(IPerror): + self.check_icmp_errror_embedded(pkt) + + def check_icmpv6_checksum(self, pkt): + """ + Check ICMPv6 checksum in IPv4 packet + + :param pkt: Packet to check ICMPv6 checksum + """ + new = pkt.__class__(str(pkt)) + if pkt.haslayer(ICMPv6DestUnreach): + del new['ICMPv6DestUnreach'].cksum + new = new.__class__(str(new)) + self.assertEqual(new['ICMPv6DestUnreach'].cksum, + pkt['ICMPv6DestUnreach'].cksum) + self.check_icmp_errror_embedded(pkt) + if pkt.haslayer(ICMPv6EchoRequest): + del new['ICMPv6EchoRequest'].cksum + new = new.__class__(str(new)) + self.assertEqual(new['ICMPv6EchoRequest'].cksum, + pkt['ICMPv6EchoRequest'].cksum) + if pkt.haslayer(ICMPv6EchoReply): + del new['ICMPv6EchoReply'].cksum + new = new.__class__(str(new)) + self.assertEqual(new['ICMPv6EchoReply'].cksum, + pkt['ICMPv6EchoReply'].cksum) + + def create_stream_in(self, in_if, out_if, ttl=64): + """ + Create packet stream for inside network + + :param in_if: Inside interface + :param out_if: Outside interface + :param ttl: TTL of generated packets + """ + pkts = [] + # TCP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + TCP(sport=self.tcp_port_in, dport=20)) + pkts.append(p) + + # UDP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + UDP(sport=self.udp_port_in, dport=20)) + pkts.append(p) + + # ICMP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + ICMP(id=self.icmp_id_in, type='echo-request')) + pkts.append(p) + + return pkts + + def compose_ip6(self, ip4, pref, plen): + """ + Compose IPv4-embedded IPv6 addresses + + :param ip4: IPv4 address + :param pref: IPv6 prefix + :param plen: IPv6 prefix length + :returns: IPv4-embedded IPv6 addresses + """ + pref_n = list(socket.inet_pton(socket.AF_INET6, pref)) + ip4_n = list(socket.inet_pton(socket.AF_INET, ip4)) + if plen == 32: + pref_n[4] = ip4_n[0] + pref_n[5] = ip4_n[1] + pref_n[6] = ip4_n[2] + pref_n[7] = ip4_n[3] + elif plen == 40: + pref_n[5] = ip4_n[0] + pref_n[6] = ip4_n[1] + pref_n[7] = ip4_n[2] + pref_n[9] = ip4_n[3] + elif plen == 48: + pref_n[6] = ip4_n[0] + pref_n[7] = ip4_n[1] + pref_n[9] = ip4_n[2] + pref_n[10] = ip4_n[3] + elif plen == 56: + pref_n[7] = ip4_n[0] + pref_n[9] = ip4_n[1] + pref_n[10] = ip4_n[2] + pref_n[11] = ip4_n[3] + elif plen == 64: + pref_n[9] = ip4_n[0] + pref_n[10] = ip4_n[1] + pref_n[11] = ip4_n[2] + pref_n[12] = ip4_n[3] + elif plen == 96: + pref_n[12] = ip4_n[0] + pref_n[13] = ip4_n[1] + pref_n[14] = ip4_n[2] + pref_n[15] = ip4_n[3] + return socket.inet_ntop(socket.AF_INET6, ''.join(pref_n)) + + def create_stream_in_ip6(self, in_if, out_if, hlim=64, pref=None, plen=0): + """ + Create IPv6 packet stream for inside network + + :param in_if: Inside interface + :param out_if: Outside interface + :param ttl: Hop Limit of generated packets + :param pref: NAT64 prefix + :param plen: NAT64 prefix length + """ + pkts = [] + if pref is None: + dst = ''.join(['64:ff9b::', out_if.remote_ip4]) + else: + dst = self.compose_ip6(out_if.remote_ip4, pref, plen) + + # TCP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IPv6(src=in_if.remote_ip6, dst=dst, hlim=hlim) / + TCP(sport=self.tcp_port_in, dport=20)) + pkts.append(p) + + # UDP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IPv6(src=in_if.remote_ip6, dst=dst, hlim=hlim) / + UDP(sport=self.udp_port_in, dport=20)) + pkts.append(p) + + # ICMP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IPv6(src=in_if.remote_ip6, dst=dst, hlim=hlim) / + ICMPv6EchoRequest(id=self.icmp_id_in)) + pkts.append(p) + + return pkts + + def create_stream_out(self, out_if, dst_ip=None, ttl=64): + """ + Create packet stream for outside network + + :param out_if: Outside interface + :param dst_ip: Destination IP address (Default use global NAT address) + :param ttl: TTL of generated packets + """ + if dst_ip is None: + dst_ip = self.nat_addr + pkts = [] + # TCP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + TCP(dport=self.tcp_port_out, sport=20)) + pkts.append(p) + + # UDP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + UDP(dport=self.udp_port_out, sport=20)) + pkts.append(p) + + # ICMP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + ICMP(id=self.icmp_id_out, type='echo-reply')) + pkts.append(p) + + return pkts + + def verify_capture_out(self, capture, nat_ip=None, same_port=False, + packet_num=3, dst_ip=None): + """ + Verify captured packets on outside network + + :param capture: Captured packets + :param nat_ip: Translated IP address (Default use global NAT address) + :param same_port: Sorce port number is not translated (Default False) + :param packet_num: Expected number of packets (Default 3) + :param dst_ip: Destination IP address (Default do not verify) + """ + if nat_ip is None: + nat_ip = self.nat_addr + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.check_ip_checksum(packet) + self.assertEqual(packet[IP].src, nat_ip) + if dst_ip is not None: + self.assertEqual(packet[IP].dst, dst_ip) + if packet.haslayer(TCP): + if same_port: + self.assertEqual(packet[TCP].sport, self.tcp_port_in) + else: + self.assertNotEqual( + packet[TCP].sport, self.tcp_port_in) + self.tcp_port_out = packet[TCP].sport + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + if same_port: + self.assertEqual(packet[UDP].sport, self.udp_port_in) + else: + self.assertNotEqual( + packet[UDP].sport, self.udp_port_in) + self.udp_port_out = packet[UDP].sport + else: + if same_port: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + else: + self.assertNotEqual(packet[ICMP].id, self.icmp_id_in) + self.icmp_id_out = packet[ICMP].id + self.check_icmp_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + + def verify_capture_in(self, capture, in_if, packet_num=3): + """ + Verify captured packets on inside network + + :param capture: Captured packets + :param in_if: Inside interface + :param packet_num: Expected number of packets (Default 3) + """ + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.check_ip_checksum(packet) + self.assertEqual(packet[IP].dst, in_if.remote_ip4) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].dport, self.tcp_port_in) + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].dport, self.udp_port_in) + else: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + self.check_icmp_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(inside network):", packet)) + raise + + def verify_capture_in_ip6(self, capture, src_ip, dst_ip, packet_num=3): + """ + Verify captured IPv6 packets on inside network + + :param capture: Captured packets + :param src_ip: Source IP + :param dst_ip: Destination IP address + :param packet_num: Expected number of packets (Default 3) + """ + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.assertEqual(packet[IPv6].src, src_ip) + self.assertEqual(packet[IPv6].dst, dst_ip) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].dport, self.tcp_port_in) + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].dport, self.udp_port_in) + self.check_udp_checksum(packet) + else: + self.assertEqual(packet[ICMPv6EchoReply].id, + self.icmp_id_in) + self.check_icmpv6_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(inside network):", packet)) + raise + + def verify_capture_no_translation(self, capture, ingress_if, egress_if): + """ + Verify captured packet that don't have to be translated + + :param capture: Captured packets + :param ingress_if: Ingress interface + :param egress_if: Egress interface + """ + for packet in capture: + try: + self.assertEqual(packet[IP].src, ingress_if.remote_ip4) + self.assertEqual(packet[IP].dst, egress_if.remote_ip4) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].sport, self.tcp_port_in) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].sport, self.udp_port_in) + else: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(inside network):", packet)) + raise + + def verify_capture_out_with_icmp_errors(self, capture, src_ip=None, + packet_num=3, icmp_type=11): + """ + Verify captured packets with ICMP errors on outside network + + :param capture: Captured packets + :param src_ip: Translated IP address or IP address of VPP + (Default use global NAT address) + :param packet_num: Expected number of packets (Default 3) + :param icmp_type: Type of error ICMP packet + we are expecting (Default 11) + """ + if src_ip is None: + src_ip = self.nat_addr + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, src_ip) + self.assertTrue(packet.haslayer(ICMP)) + icmp = packet[ICMP] + self.assertEqual(icmp.type, icmp_type) + self.assertTrue(icmp.haslayer(IPerror)) + inner_ip = icmp[IPerror] + if inner_ip.haslayer(TCPerror): + self.assertEqual(inner_ip[TCPerror].dport, + self.tcp_port_out) + elif inner_ip.haslayer(UDPerror): + self.assertEqual(inner_ip[UDPerror].dport, + self.udp_port_out) + else: + self.assertEqual(inner_ip[ICMPerror].id, self.icmp_id_out) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + + def verify_capture_in_with_icmp_errors(self, capture, in_if, packet_num=3, + icmp_type=11): + """ + Verify captured packets with ICMP errors on inside network + + :param capture: Captured packets + :param in_if: Inside interface + :param packet_num: Expected number of packets (Default 3) + :param icmp_type: Type of error ICMP packet + we are expecting (Default 11) + """ + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.assertEqual(packet[IP].dst, in_if.remote_ip4) + self.assertTrue(packet.haslayer(ICMP)) + icmp = packet[ICMP] + self.assertEqual(icmp.type, icmp_type) + self.assertTrue(icmp.haslayer(IPerror)) + inner_ip = icmp[IPerror] + if inner_ip.haslayer(TCPerror): + self.assertEqual(inner_ip[TCPerror].sport, + self.tcp_port_in) + elif inner_ip.haslayer(UDPerror): + self.assertEqual(inner_ip[UDPerror].sport, + self.udp_port_in) + else: + self.assertEqual(inner_ip[ICMPerror].id, self.icmp_id_in) + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(inside network):", packet)) + raise + + def verify_ipfix_nat44_ses(self, data): + """ + Verify IPFIX NAT44 session create/delete event + + :param data: Decoded IPFIX data records + """ + nat44_ses_create_num = 0 + nat44_ses_delete_num = 0 + self.assertEqual(6, len(data)) + for record in data: + # natEvent + self.assertIn(ord(record[230]), [4, 5]) + if ord(record[230]) == 4: + nat44_ses_create_num += 1 + else: + nat44_ses_delete_num += 1 + # sourceIPv4Address + self.assertEqual(self.pg0.remote_ip4n, record[8]) + # postNATSourceIPv4Address + self.assertEqual(socket.inet_pton(socket.AF_INET, self.nat_addr), + record[225]) + # ingressVRFID + self.assertEqual(struct.pack("!I", 0), record[234]) + # protocolIdentifier/sourceTransportPort/postNAPTSourceTransportPort + if IP_PROTOS.icmp == ord(record[4]): + self.assertEqual(struct.pack("!H", self.icmp_id_in), record[7]) + self.assertEqual(struct.pack("!H", self.icmp_id_out), + record[227]) + elif IP_PROTOS.tcp == ord(record[4]): + self.assertEqual(struct.pack("!H", self.tcp_port_in), + record[7]) + self.assertEqual(struct.pack("!H", self.tcp_port_out), + record[227]) + elif IP_PROTOS.udp == ord(record[4]): + self.assertEqual(struct.pack("!H", self.udp_port_in), + record[7]) + self.assertEqual(struct.pack("!H", self.udp_port_out), + record[227]) + else: + self.fail("Invalid protocol") + self.assertEqual(3, nat44_ses_create_num) + self.assertEqual(3, nat44_ses_delete_num) + + def verify_ipfix_addr_exhausted(self, data): + """ + Verify IPFIX NAT addresses event + + :param data: Decoded IPFIX data records + """ + self.assertEqual(1, len(data)) + record = data[0] + # natEvent + self.assertEqual(ord(record[230]), 3) + # natPoolID + self.assertEqual(struct.pack("!I", 0), record[283]) + + +class TestNAT44(MethodHolder): + """ NAT44 Test Cases """ + + @classmethod + def setUpClass(cls): + super(TestNAT44, cls).setUpClass() + + try: + cls.tcp_port_in = 6303 + cls.tcp_port_out = 6303 + cls.udp_port_in = 6304 + cls.udp_port_out = 6304 + cls.icmp_id_in = 6305 + cls.icmp_id_out = 6305 + cls.nat_addr = '10.0.0.3' + cls.ipfix_src_port = 4739 + cls.ipfix_domain_id = 1 + + cls.create_pg_interfaces(range(9)) + cls.interfaces = list(cls.pg_interfaces[0:4]) + + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + cls.pg0.generate_remote_hosts(3) + cls.pg0.configure_ipv4_neighbors() + + cls.overlapping_interfaces = list(list(cls.pg_interfaces[4:7])) + cls.vapi.ip_table_add_del(10, is_add=1) + cls.vapi.ip_table_add_del(20, is_add=1) + + cls.pg4._local_ip4 = "172.16.255.1" + cls.pg4._local_ip4n = socket.inet_pton(socket.AF_INET, i.local_ip4) + cls.pg4._remote_hosts[0]._ip4 = "172.16.255.2" + cls.pg4.set_table_ip4(10) + cls.pg5._local_ip4 = "172.17.255.3" + cls.pg5._local_ip4n = socket.inet_pton(socket.AF_INET, i.local_ip4) + cls.pg5._remote_hosts[0]._ip4 = "172.17.255.4" + cls.pg5.set_table_ip4(10) + cls.pg6._local_ip4 = "172.16.255.1" + cls.pg6._local_ip4n = socket.inet_pton(socket.AF_INET, i.local_ip4) + cls.pg6._remote_hosts[0]._ip4 = "172.16.255.2" + cls.pg6.set_table_ip4(20) + for i in cls.overlapping_interfaces: + i.config_ip4() + i.admin_up() + i.resolve_arp() + + cls.pg7.admin_up() + cls.pg8.admin_up() + + except Exception: + super(TestNAT44, cls).tearDownClass() + raise + + def clear_nat44(self): + """ + Clear NAT44 configuration. + """ + # I found no elegant way to do this + self.vapi.ip_add_del_route(dst_address=self.pg7.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg7.remote_ip4n, + next_hop_sw_if_index=self.pg7.sw_if_index, + is_add=0) + self.vapi.ip_add_del_route(dst_address=self.pg8.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg8.remote_ip4n, + next_hop_sw_if_index=self.pg8.sw_if_index, + is_add=0) + + for intf in [self.pg7, self.pg8]: + neighbors = self.vapi.ip_neighbor_dump(intf.sw_if_index) + for n in neighbors: + self.vapi.ip_neighbor_add_del(intf.sw_if_index, + n.mac_address, + n.ip_address, + is_add=0) + + if self.pg7.has_ip4_config: + self.pg7.unconfig_ip4() + + interfaces = self.vapi.nat44_interface_addr_dump() + for intf in interfaces: + self.vapi.nat44_add_interface_addr(intf.sw_if_index, is_add=0) + + self.vapi.nat_ipfix(enable=0, src_port=self.ipfix_src_port, + domain_id=self.ipfix_domain_id) + self.ipfix_src_port = 4739 + self.ipfix_domain_id = 1 + + interfaces = self.vapi.nat44_interface_dump() + for intf in interfaces: + self.vapi.nat44_interface_add_del_feature(intf.sw_if_index, + intf.is_inside, + is_add=0) + + interfaces = self.vapi.nat44_interface_output_feature_dump() + for intf in interfaces: + self.vapi.nat44_interface_add_del_output_feature(intf.sw_if_index, + intf.is_inside, + is_add=0) + + static_mappings = self.vapi.nat44_static_mapping_dump() + for sm in static_mappings: + self.vapi.nat44_add_del_static_mapping( + sm.local_ip_address, + sm.external_ip_address, + local_port=sm.local_port, + external_port=sm.external_port, + addr_only=sm.addr_only, + vrf_id=sm.vrf_id, + protocol=sm.protocol, + is_add=0) + + lb_static_mappings = self.vapi.nat44_lb_static_mapping_dump() + for lb_sm in lb_static_mappings: + self.vapi.nat44_add_del_lb_static_mapping( + lb_sm.external_addr, + lb_sm.external_port, + lb_sm.protocol, + lb_sm.vrf_id, + is_add=0, + local_num=0, + locals=[]) + + adresses = self.vapi.nat44_address_dump() + for addr in adresses: + self.vapi.nat44_add_del_address_range(addr.ip_address, + addr.ip_address, + is_add=0) + + def nat44_add_static_mapping(self, local_ip, external_ip='0.0.0.0', + local_port=0, external_port=0, vrf_id=0, + is_add=1, external_sw_if_index=0xFFFFFFFF, + proto=0): + """ + Add/delete NAT44 static mapping + + :param local_ip: Local IP address + :param external_ip: External IP address + :param local_port: Local port number (Optional) + :param external_port: External port number (Optional) + :param vrf_id: VRF ID (Default 0) + :param is_add: 1 if add, 0 if delete (Default add) + :param external_sw_if_index: External interface instead of IP address + :param proto: IP protocol (Mandatory if port specified) + """ + addr_only = 1 + if local_port and external_port: + addr_only = 0 + l_ip = socket.inet_pton(socket.AF_INET, local_ip) + e_ip = socket.inet_pton(socket.AF_INET, external_ip) + self.vapi.nat44_add_del_static_mapping( + l_ip, + e_ip, + external_sw_if_index, + local_port, + external_port, + addr_only, + vrf_id, + proto, + is_add) + + def nat44_add_address(self, ip, is_add=1, vrf_id=0xFFFFFFFF): + """ + Add/delete NAT44 address + + :param ip: IP address + :param is_add: 1 if add, 0 if delete (Default add) + """ + nat_addr = socket.inet_pton(socket.AF_INET, ip) + self.vapi.nat44_add_del_address_range(nat_addr, nat_addr, is_add, + vrf_id=vrf_id) + + def test_dynamic(self): + """ NAT44 dynamic translation test """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in + pkts = self.create_stream_out(self.pg1) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + def test_dynamic_icmp_errors_in2out_ttl_1(self): + """ NAT44 handling of client packets with TTL=1 """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # Client side - generate traffic + pkts = self.create_stream_in(self.pg0, self.pg1, ttl=1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Client side - verify ICMP type 11 packets + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in_with_icmp_errors(capture, self.pg0) + + def test_dynamic_icmp_errors_out2in_ttl_1(self): + """ NAT44 handling of server packets with TTL=1 """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # Client side - create sessions + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Server side - generate traffic + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + pkts = self.create_stream_out(self.pg1, ttl=1) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Server side - verify ICMP type 11 packets + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out_with_icmp_errors(capture, + src_ip=self.pg1.local_ip4) + + def test_dynamic_icmp_errors_in2out_ttl_2(self): + """ NAT44 handling of error responses to client packets with TTL=2 """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # Client side - generate traffic + pkts = self.create_stream_in(self.pg0, self.pg1, ttl=2) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Server side - simulate ICMP type 11 response + capture = self.pg1.get_capture(len(pkts)) + pkts = [Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + ICMP(type=11) / packet[IP] for packet in capture] + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Client side - verify ICMP type 11 packets + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in_with_icmp_errors(capture, self.pg0) + + def test_dynamic_icmp_errors_out2in_ttl_2(self): + """ NAT44 handling of error responses to server packets with TTL=2 """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # Client side - create sessions + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Server side - generate traffic + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + pkts = self.create_stream_out(self.pg1, ttl=2) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Client side - simulate ICMP type 11 response + capture = self.pg0.get_capture(len(pkts)) + pkts = [Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + ICMP(type=11) / packet[IP] for packet in capture] + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Server side - verify ICMP type 11 packets + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out_with_icmp_errors(capture) + + def test_ping_out_interface_from_outside(self): + """ Ping NAT44 out interface from outside network """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + p = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.pg1.local_ip4) / + ICMP(id=self.icmp_id_out, type='echo-request')) + pkts = [p] + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.assertEqual(1, len(capture)) + packet = capture[0] + try: + self.assertEqual(packet[IP].src, self.pg1.local_ip4) + self.assertEqual(packet[IP].dst, self.pg1.remote_ip4) + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + self.assertEqual(packet[ICMP].type, 0) # echo reply + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + + def test_ping_internal_host_from_outside(self): + """ Ping internal host from outside network """ + + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # out2in + pkt = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr, ttl=64) / + ICMP(id=self.icmp_id_out, type='echo-request')) + self.pg1.add_stream(pkt) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + self.verify_capture_in(capture, self.pg0, packet_num=1) + self.assert_equal(capture[0][IP].proto, IP_PROTOS.icmp) + + # in2out + pkt = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4, ttl=64) / + ICMP(id=self.icmp_id_in, type='echo-reply')) + self.pg0.add_stream(pkt) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + self.verify_capture_out(capture, same_port=True, packet_num=1) + self.assert_equal(capture[0][IP].proto, IP_PROTOS.icmp) + + def test_static_in(self): + """ 1:1 NAT initialized from inside network """ + + nat_ip = "10.0.0.10" + self.tcp_port_out = 6303 + self.udp_port_out = 6304 + self.icmp_id_out = 6305 + + self.nat44_add_static_mapping(self.pg0.remote_ip4, nat_ip) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip, True) + + # out2in + pkts = self.create_stream_out(self.pg1, nat_ip) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + def test_static_out(self): + """ 1:1 NAT initialized from outside network """ + + nat_ip = "10.0.0.20" + self.tcp_port_out = 6303 + self.udp_port_out = 6304 + self.icmp_id_out = 6305 + + self.nat44_add_static_mapping(self.pg0.remote_ip4, nat_ip) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # out2in + pkts = self.create_stream_out(self.pg1, nat_ip) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip, True) + + def test_static_with_port_in(self): + """ 1:1 NAPT initialized from inside network """ + + self.tcp_port_out = 3606 + self.udp_port_out = 3607 + self.icmp_id_out = 3608 + + self.nat44_add_address(self.nat_addr) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.tcp_port_in, self.tcp_port_out, + proto=IP_PROTOS.tcp) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.udp_port_in, self.udp_port_out, + proto=IP_PROTOS.udp) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.icmp_id_in, self.icmp_id_out, + proto=IP_PROTOS.icmp) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in + pkts = self.create_stream_out(self.pg1) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + def test_static_with_port_out(self): + """ 1:1 NAPT initialized from outside network """ + + self.tcp_port_out = 30606 + self.udp_port_out = 30607 + self.icmp_id_out = 30608 + + self.nat44_add_address(self.nat_addr) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.tcp_port_in, self.tcp_port_out, + proto=IP_PROTOS.tcp) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.udp_port_in, self.udp_port_out, + proto=IP_PROTOS.udp) + self.nat44_add_static_mapping(self.pg0.remote_ip4, self.nat_addr, + self.icmp_id_in, self.icmp_id_out, + proto=IP_PROTOS.icmp) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # out2in + pkts = self.create_stream_out(self.pg1) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + + def test_static_vrf_aware(self): + """ 1:1 NAT VRF awareness """ + + nat_ip1 = "10.0.0.30" + nat_ip2 = "10.0.0.40" + self.tcp_port_out = 6303 + self.udp_port_out = 6304 + self.icmp_id_out = 6305 + + self.nat44_add_static_mapping(self.pg4.remote_ip4, nat_ip1, + vrf_id=10) + self.nat44_add_static_mapping(self.pg0.remote_ip4, nat_ip2, + vrf_id=10) + self.vapi.nat44_interface_add_del_feature(self.pg3.sw_if_index, + is_inside=0) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg4.sw_if_index) + + # inside interface VRF match NAT44 static mapping VRF + pkts = self.create_stream_in(self.pg4, self.pg3) + self.pg4.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip1, True) + + # inside interface VRF don't match NAT44 static mapping VRF (packets + # are dropped) + pkts = self.create_stream_in(self.pg0, self.pg3) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg3.assert_nothing_captured() + + def test_static_lb(self): + """ NAT44 local service load balancing """ + external_addr_n = socket.inet_pton(socket.AF_INET, self.nat_addr) + external_port = 80 + local_port = 8080 + server1 = self.pg0.remote_hosts[0] + server2 = self.pg0.remote_hosts[1] + + locals = [{'addr': server1.ip4n, + 'port': local_port, + 'probability': 70}, + {'addr': server2.ip4n, + 'port': local_port, + 'probability': 30}] + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_add_del_lb_static_mapping(external_addr_n, + external_port, + IP_PROTOS.tcp, + local_num=len(locals), + locals=locals) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # from client to service + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + TCP(sport=12345, dport=external_port)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + server = None + try: + ip = p[IP] + tcp = p[TCP] + self.assertIn(ip.dst, [server1.ip4, server2.ip4]) + if ip.dst == server1.ip4: + server = server1 + else: + server = server2 + self.assertEqual(tcp.dport, local_port) + self.check_tcp_checksum(p) + self.check_ip_checksum(p) + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # from service back to client + p = (Ether(src=server.mac, dst=self.pg0.local_mac) / + IP(src=server.ip4, dst=self.pg1.remote_ip4) / + TCP(sport=local_port, dport=12345)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(tcp.sport, external_port) + self.check_tcp_checksum(p) + self.check_ip_checksum(p) + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # multiple clients + server1_n = 0 + server2_n = 0 + clients = ip4_range(self.pg1.remote_ip4, 10, 20) + pkts = [] + for client in clients: + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=client, dst=self.nat_addr) / + TCP(sport=12345, dport=external_port)) + pkts.append(p) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for p in capture: + if p[IP].dst == server1.ip4: + server1_n += 1 + else: + server2_n += 1 + self.assertTrue(server1_n > server2_n) + + def test_multiple_inside_interfaces(self): + """ NAT44 multiple non-overlapping address space inside interfaces """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg3.sw_if_index, + is_inside=0) + + # between two NAT44 inside interfaces (no translation) + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_no_translation(capture, self.pg0, self.pg1) + + # from NAT44 inside to interface without NAT44 feature (no translation) + pkts = self.create_stream_in(self.pg0, self.pg2) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_no_translation(capture, self.pg0, self.pg2) + + # in2out 1st interface + pkts = self.create_stream_in(self.pg0, self.pg3) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in 1st interface + pkts = self.create_stream_out(self.pg3) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + # in2out 2nd interface + pkts = self.create_stream_in(self.pg1, self.pg3) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in 2nd interface + pkts = self.create_stream_out(self.pg3) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg1) + + def test_inside_overlapping_interfaces(self): + """ NAT44 multiple inside interfaces with overlapping address space """ + + static_nat_ip = "10.0.0.10" + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg3.sw_if_index, + is_inside=0) + self.vapi.nat44_interface_add_del_feature(self.pg4.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg5.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg6.sw_if_index) + self.nat44_add_static_mapping(self.pg6.remote_ip4, static_nat_ip, + vrf_id=20) + + # between NAT44 inside interfaces with same VRF (no translation) + pkts = self.create_stream_in(self.pg4, self.pg5) + self.pg4.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg5.get_capture(len(pkts)) + self.verify_capture_no_translation(capture, self.pg4, self.pg5) + + # between NAT44 inside interfaces with different VRF (hairpinning) + p = (Ether(src=self.pg4.remote_mac, dst=self.pg4.local_mac) / + IP(src=self.pg4.remote_ip4, dst=static_nat_ip) / + TCP(sport=1234, dport=5678)) + self.pg4.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg6.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(ip.dst, self.pg6.remote_ip4) + self.assertNotEqual(tcp.sport, 1234) + self.assertEqual(tcp.dport, 5678) + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # in2out 1st interface + pkts = self.create_stream_in(self.pg4, self.pg3) + self.pg4.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in 1st interface + pkts = self.create_stream_out(self.pg3) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg4.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg4) + + # in2out 2nd interface + pkts = self.create_stream_in(self.pg5, self.pg3) + self.pg5.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in 2nd interface + pkts = self.create_stream_out(self.pg3) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg5.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg5) + + # pg5 session dump + addresses = self.vapi.nat44_address_dump() + self.assertEqual(len(addresses), 1) + sessions = self.vapi.nat44_user_session_dump(self.pg5.remote_ip4n, 10) + self.assertEqual(len(sessions), 3) + for session in sessions: + self.assertFalse(session.is_static) + self.assertEqual(session.inside_ip_address[0:4], + self.pg5.remote_ip4n) + self.assertEqual(session.outside_ip_address, + addresses[0].ip_address) + self.assertEqual(sessions[0].protocol, IP_PROTOS.tcp) + self.assertEqual(sessions[1].protocol, IP_PROTOS.udp) + self.assertEqual(sessions[2].protocol, IP_PROTOS.icmp) + self.assertEqual(sessions[0].inside_port, self.tcp_port_in) + self.assertEqual(sessions[1].inside_port, self.udp_port_in) + self.assertEqual(sessions[2].inside_port, self.icmp_id_in) + self.assertEqual(sessions[0].outside_port, self.tcp_port_out) + self.assertEqual(sessions[1].outside_port, self.udp_port_out) + self.assertEqual(sessions[2].outside_port, self.icmp_id_out) + + # in2out 3rd interface + pkts = self.create_stream_in(self.pg6, self.pg3) + self.pg6.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture, static_nat_ip, True) + + # out2in 3rd interface + pkts = self.create_stream_out(self.pg3, static_nat_ip) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg6.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg6) + + # general user and session dump verifications + users = self.vapi.nat44_user_dump() + self.assertTrue(len(users) >= 3) + addresses = self.vapi.nat44_address_dump() + self.assertEqual(len(addresses), 1) + for user in users: + sessions = self.vapi.nat44_user_session_dump(user.ip_address, + user.vrf_id) + for session in sessions: + self.assertEqual(user.ip_address, session.inside_ip_address) + self.assertTrue(session.total_bytes > session.total_pkts > 0) + self.assertTrue(session.protocol in + [IP_PROTOS.tcp, IP_PROTOS.udp, + IP_PROTOS.icmp]) + + # pg4 session dump + sessions = self.vapi.nat44_user_session_dump(self.pg4.remote_ip4n, 10) + self.assertTrue(len(sessions) >= 4) + for session in sessions: + self.assertFalse(session.is_static) + self.assertEqual(session.inside_ip_address[0:4], + self.pg4.remote_ip4n) + self.assertEqual(session.outside_ip_address, + addresses[0].ip_address) + + # pg6 session dump + sessions = self.vapi.nat44_user_session_dump(self.pg6.remote_ip4n, 20) + self.assertTrue(len(sessions) >= 3) + for session in sessions: + self.assertTrue(session.is_static) + self.assertEqual(session.inside_ip_address[0:4], + self.pg6.remote_ip4n) + self.assertEqual(map(ord, session.outside_ip_address[0:4]), + map(int, static_nat_ip.split('.'))) + self.assertTrue(session.inside_port in + [self.tcp_port_in, self.udp_port_in, + self.icmp_id_in]) + + def test_hairpinning(self): + """ NAT44 hairpinning - 1:1 NAPT """ + + host = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + host_in_port = 1234 + host_out_port = 0 + server_in_port = 5678 + server_out_port = 8765 + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + # add static mapping for server + self.nat44_add_static_mapping(server.ip4, self.nat_addr, + server_in_port, server_out_port, + proto=IP_PROTOS.tcp) + + # send packet from host to server + p = (Ether(src=host.mac, dst=self.pg0.local_mac) / + IP(src=host.ip4, dst=self.nat_addr) / + TCP(sport=host_in_port, dport=server_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(ip.dst, server.ip4) + self.assertNotEqual(tcp.sport, host_in_port) + self.assertEqual(tcp.dport, server_in_port) + self.check_tcp_checksum(p) + host_out_port = tcp.sport + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # send reply from server to host + p = (Ether(src=server.mac, dst=self.pg0.local_mac) / + IP(src=server.ip4, dst=self.nat_addr) / + TCP(sport=server_in_port, dport=host_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(ip.dst, host.ip4) + self.assertEqual(tcp.sport, server_out_port) + self.assertEqual(tcp.dport, host_in_port) + self.check_tcp_checksum(p) + except: + self.logger.error(ppp("Unexpected or invalid packet:"), p) + raise + + def test_hairpinning2(self): + """ NAT44 hairpinning - 1:1 NAT""" + + server1_nat_ip = "10.0.0.10" + server2_nat_ip = "10.0.0.11" + host = self.pg0.remote_hosts[0] + server1 = self.pg0.remote_hosts[1] + server2 = self.pg0.remote_hosts[2] + server_tcp_port = 22 + server_udp_port = 20 + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # add static mapping for servers + self.nat44_add_static_mapping(server1.ip4, server1_nat_ip) + self.nat44_add_static_mapping(server2.ip4, server2_nat_ip) + + # host to server1 + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=host.ip4, dst=server1_nat_ip) / + TCP(sport=self.tcp_port_in, dport=server_tcp_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=host.ip4, dst=server1_nat_ip) / + UDP(sport=self.udp_port_in, dport=server_udp_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=host.ip4, dst=server1_nat_ip) / + ICMP(id=self.icmp_id_in, type='echo-request')) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, self.nat_addr) + self.assertEqual(packet[IP].dst, server1.ip4) + if packet.haslayer(TCP): + self.assertNotEqual(packet[TCP].sport, self.tcp_port_in) + self.assertEqual(packet[TCP].dport, server_tcp_port) + self.tcp_port_out = packet[TCP].sport + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertNotEqual(packet[UDP].sport, self.udp_port_in) + self.assertEqual(packet[UDP].dport, server_udp_port) + self.udp_port_out = packet[UDP].sport + else: + self.assertNotEqual(packet[ICMP].id, self.icmp_id_in) + self.icmp_id_out = packet[ICMP].id + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server1 to host + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=self.nat_addr) / + TCP(sport=server_tcp_port, dport=self.tcp_port_out)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=self.nat_addr) / + UDP(sport=server_udp_port, dport=self.udp_port_out)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=self.nat_addr) / + ICMP(id=self.icmp_id_out, type='echo-reply')) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, server1_nat_ip) + self.assertEqual(packet[IP].dst, host.ip4) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].dport, self.tcp_port_in) + self.assertEqual(packet[TCP].sport, server_tcp_port) + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].dport, self.udp_port_in) + self.assertEqual(packet[UDP].sport, server_udp_port) + else: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server2 to server1 + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server2.ip4, dst=server1_nat_ip) / + TCP(sport=self.tcp_port_in, dport=server_tcp_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server2.ip4, dst=server1_nat_ip) / + UDP(sport=self.udp_port_in, dport=server_udp_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server2.ip4, dst=server1_nat_ip) / + ICMP(id=self.icmp_id_in, type='echo-request')) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, server2_nat_ip) + self.assertEqual(packet[IP].dst, server1.ip4) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].sport, self.tcp_port_in) + self.assertEqual(packet[TCP].dport, server_tcp_port) + self.tcp_port_out = packet[TCP].sport + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].sport, self.udp_port_in) + self.assertEqual(packet[UDP].dport, server_udp_port) + self.udp_port_out = packet[UDP].sport + else: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + self.icmp_id_out = packet[ICMP].id + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server1 to server2 + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=server2_nat_ip) / + TCP(sport=server_tcp_port, dport=self.tcp_port_out)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=server2_nat_ip) / + UDP(sport=server_udp_port, dport=self.udp_port_out)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=server1.ip4, dst=server2_nat_ip) / + ICMP(id=self.icmp_id_out, type='echo-reply')) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, server1_nat_ip) + self.assertEqual(packet[IP].dst, server2.ip4) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].dport, self.tcp_port_in) + self.assertEqual(packet[TCP].sport, server_tcp_port) + self.check_tcp_checksum(packet) + elif packet.haslayer(UDP): + self.assertEqual(packet[UDP].dport, self.udp_port_in) + self.assertEqual(packet[UDP].sport, server_udp_port) + else: + self.assertEqual(packet[ICMP].id, self.icmp_id_in) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_max_translations_per_user(self): + """ MAX translations per user - recycle the least recently used """ + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # get maximum number of translations per user + nat44_config = self.vapi.nat_show_config() + + # send more than maximum number of translations per user packets + pkts_num = nat44_config.max_translations_per_user + 5 + pkts = [] + for port in range(0, pkts_num): + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=1025 + port)) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # verify number of translated packet + self.pg1.get_capture(pkts_num) + + def test_interface_addr(self): + """ Acquire NAT44 addresses from interface """ + self.vapi.nat44_add_interface_addr(self.pg7.sw_if_index) + + # no address in NAT pool + adresses = self.vapi.nat44_address_dump() + self.assertEqual(0, len(adresses)) + + # configure interface address and check NAT address pool + self.pg7.config_ip4() + adresses = self.vapi.nat44_address_dump() + self.assertEqual(1, len(adresses)) + self.assertEqual(adresses[0].ip_address[0:4], self.pg7.local_ip4n) + + # remove interface address and check NAT address pool + self.pg7.unconfig_ip4() + adresses = self.vapi.nat44_address_dump() + self.assertEqual(0, len(adresses)) + + def test_interface_addr_static_mapping(self): + """ Static mapping with addresses from interface """ + self.vapi.nat44_add_interface_addr(self.pg7.sw_if_index) + self.nat44_add_static_mapping( + '1.2.3.4', + external_sw_if_index=self.pg7.sw_if_index) + + # static mappings with external interface + static_mappings = self.vapi.nat44_static_mapping_dump() + self.assertEqual(1, len(static_mappings)) + self.assertEqual(self.pg7.sw_if_index, + static_mappings[0].external_sw_if_index) + + # configure interface address and check static mappings + self.pg7.config_ip4() + static_mappings = self.vapi.nat44_static_mapping_dump() + self.assertEqual(1, len(static_mappings)) + self.assertEqual(static_mappings[0].external_ip_address[0:4], + self.pg7.local_ip4n) + self.assertEqual(0xFFFFFFFF, static_mappings[0].external_sw_if_index) + + # remove interface address and check static mappings + self.pg7.unconfig_ip4() + static_mappings = self.vapi.nat44_static_mapping_dump() + self.assertEqual(0, len(static_mappings)) + + def test_ipfix_nat44_sess(self): + """ IPFIX logging NAT44 session created/delted """ + self.ipfix_domain_id = 10 + self.ipfix_src_port = 20202 + colector_port = 30303 + bind_layers(UDP, IPFIX, dport=30303) + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + self.vapi.set_ipfix_exporter(collector_address=self.pg3.remote_ip4n, + src_address=self.pg3.local_ip4n, + path_mtu=512, + template_interval=10, + collector_port=colector_port) + self.vapi.nat_ipfix(domain_id=self.ipfix_domain_id, + src_port=self.ipfix_src_port) + + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture) + self.nat44_add_address(self.nat_addr, is_add=0) + self.vapi.cli("ipfix flush") # FIXME this should be an API call + capture = self.pg3.get_capture(3) + ipfix = IPFIXDecoder() + # first load template + for p in capture: + self.assertTrue(p.haslayer(IPFIX)) + self.assertEqual(p[IP].src, self.pg3.local_ip4) + self.assertEqual(p[IP].dst, self.pg3.remote_ip4) + self.assertEqual(p[UDP].sport, self.ipfix_src_port) + self.assertEqual(p[UDP].dport, colector_port) + self.assertEqual(p[IPFIX].observationDomainID, + self.ipfix_domain_id) + if p.haslayer(Template): + ipfix.add_template(p.getlayer(Template)) + # verify events in data set + for p in capture: + if p.haslayer(Data): + data = ipfix.decode_data_set(p.getlayer(Set)) + self.verify_ipfix_nat44_ses(data) + + def test_ipfix_addr_exhausted(self): + """ IPFIX logging NAT addresses exhausted """ + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + self.vapi.set_ipfix_exporter(collector_address=self.pg3.remote_ip4n, + src_address=self.pg3.local_ip4n, + path_mtu=512, + template_interval=10) + self.vapi.nat_ipfix(domain_id=self.ipfix_domain_id, + src_port=self.ipfix_src_port) + + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=3025)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(0) + self.vapi.cli("ipfix flush") # FIXME this should be an API call + capture = self.pg3.get_capture(3) + ipfix = IPFIXDecoder() + # first load template + for p in capture: + self.assertTrue(p.haslayer(IPFIX)) + self.assertEqual(p[IP].src, self.pg3.local_ip4) + self.assertEqual(p[IP].dst, self.pg3.remote_ip4) + self.assertEqual(p[UDP].sport, self.ipfix_src_port) + self.assertEqual(p[UDP].dport, 4739) + self.assertEqual(p[IPFIX].observationDomainID, + self.ipfix_domain_id) + if p.haslayer(Template): + ipfix.add_template(p.getlayer(Template)) + # verify events in data set + for p in capture: + if p.haslayer(Data): + data = ipfix.decode_data_set(p.getlayer(Set)) + self.verify_ipfix_addr_exhausted(data) + + def test_pool_addr_fib(self): + """ NAT44 add pool addresses to FIB """ + static_addr = '10.0.0.10' + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + self.nat44_add_static_mapping(self.pg0.remote_ip4, static_addr) + + # NAT44 address + p = (Ether(src=self.pg1.remote_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(op=ARP.who_has, pdst=self.nat_addr, + psrc=self.pg1.remote_ip4, hwsrc=self.pg1.remote_mac)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + self.assertTrue(capture[0].haslayer(ARP)) + self.assertTrue(capture[0][ARP].op, ARP.is_at) + + # 1:1 NAT address + p = (Ether(src=self.pg1.remote_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(op=ARP.who_has, pdst=static_addr, + psrc=self.pg1.remote_ip4, hwsrc=self.pg1.remote_mac)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + self.assertTrue(capture[0].haslayer(ARP)) + self.assertTrue(capture[0][ARP].op, ARP.is_at) + + # send ARP to non-NAT44 interface + p = (Ether(src=self.pg2.remote_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(op=ARP.who_has, pdst=self.nat_addr, + psrc=self.pg2.remote_ip4, hwsrc=self.pg2.remote_mac)) + self.pg2.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(0) + + # remove addresses and verify + self.nat44_add_address(self.nat_addr, is_add=0) + self.nat44_add_static_mapping(self.pg0.remote_ip4, static_addr, + is_add=0) + + p = (Ether(src=self.pg1.remote_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(op=ARP.who_has, pdst=self.nat_addr, + psrc=self.pg1.remote_ip4, hwsrc=self.pg1.remote_mac)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(0) + + p = (Ether(src=self.pg1.remote_mac, dst='ff:ff:ff:ff:ff:ff') / + ARP(op=ARP.who_has, pdst=static_addr, + psrc=self.pg1.remote_ip4, hwsrc=self.pg1.remote_mac)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(0) + + def test_vrf_mode(self): + """ NAT44 tenant VRF aware address pool mode """ + + vrf_id1 = 1 + vrf_id2 = 2 + nat_ip1 = "10.0.0.10" + nat_ip2 = "10.0.0.11" + + self.pg0.unconfig_ip4() + self.pg1.unconfig_ip4() + self.vapi.ip_table_add_del(vrf_id1, is_add=1) + self.vapi.ip_table_add_del(vrf_id2, is_add=1) + self.pg0.set_table_ip4(vrf_id1) + self.pg1.set_table_ip4(vrf_id2) + self.pg0.config_ip4() + self.pg1.config_ip4() + + self.nat44_add_address(nat_ip1, vrf_id=vrf_id1) + self.nat44_add_address(nat_ip2, vrf_id=vrf_id2) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg2.sw_if_index, + is_inside=0) + + # first VRF + pkts = self.create_stream_in(self.pg0, self.pg2) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip1) + + # second VRF + pkts = self.create_stream_in(self.pg1, self.pg2) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip2) + + self.pg0.unconfig_ip4() + self.pg1.unconfig_ip4() + self.pg0.set_table_ip4(0) + self.pg1.set_table_ip4(0) + self.vapi.ip_table_add_del(vrf_id1, is_add=0) + self.vapi.ip_table_add_del(vrf_id2, is_add=0) + + def test_vrf_feature_independent(self): + """ NAT44 tenant VRF independent address pool mode """ + + nat_ip1 = "10.0.0.10" + nat_ip2 = "10.0.0.11" + + self.nat44_add_address(nat_ip1) + self.nat44_add_address(nat_ip2) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg2.sw_if_index, + is_inside=0) + + # first VRF + pkts = self.create_stream_in(self.pg0, self.pg2) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip1) + + # second VRF + pkts = self.create_stream_in(self.pg1, self.pg2) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip1) + + def test_dynamic_ipless_interfaces(self): + """ NAT44 interfaces without configured IP address """ + + self.vapi.ip_neighbor_add_del(self.pg7.sw_if_index, + mactobinary(self.pg7.remote_mac), + self.pg7.remote_ip4n, + is_static=1) + self.vapi.ip_neighbor_add_del(self.pg8.sw_if_index, + mactobinary(self.pg8.remote_mac), + self.pg8.remote_ip4n, + is_static=1) + + self.vapi.ip_add_del_route(dst_address=self.pg7.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg7.remote_ip4n, + next_hop_sw_if_index=self.pg7.sw_if_index) + self.vapi.ip_add_del_route(dst_address=self.pg8.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg8.remote_ip4n, + next_hop_sw_if_index=self.pg8.sw_if_index) + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg7.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg8.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg7, self.pg8) + self.pg7.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg8.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in + pkts = self.create_stream_out(self.pg8, self.nat_addr) + self.pg8.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg7.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg7) + + def test_static_ipless_interfaces(self): + """ NAT44 interfaces without configured IP address - 1:1 NAT """ + + self.vapi.ip_neighbor_add_del(self.pg7.sw_if_index, + mactobinary(self.pg7.remote_mac), + self.pg7.remote_ip4n, + is_static=1) + self.vapi.ip_neighbor_add_del(self.pg8.sw_if_index, + mactobinary(self.pg8.remote_mac), + self.pg8.remote_ip4n, + is_static=1) + + self.vapi.ip_add_del_route(dst_address=self.pg7.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg7.remote_ip4n, + next_hop_sw_if_index=self.pg7.sw_if_index) + self.vapi.ip_add_del_route(dst_address=self.pg8.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg8.remote_ip4n, + next_hop_sw_if_index=self.pg8.sw_if_index) + + self.nat44_add_static_mapping(self.pg7.remote_ip4, self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg7.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg8.sw_if_index, + is_inside=0) + + # out2in + pkts = self.create_stream_out(self.pg8) + self.pg8.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg7.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg7) + + # in2out + pkts = self.create_stream_in(self.pg7, self.pg8) + self.pg7.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg8.get_capture(len(pkts)) + self.verify_capture_out(capture, self.nat_addr, True) + + def test_static_with_port_ipless_interfaces(self): + """ NAT44 interfaces without configured IP address - 1:1 NAPT """ + + self.tcp_port_out = 30606 + self.udp_port_out = 30607 + self.icmp_id_out = 30608 + + self.vapi.ip_neighbor_add_del(self.pg7.sw_if_index, + mactobinary(self.pg7.remote_mac), + self.pg7.remote_ip4n, + is_static=1) + self.vapi.ip_neighbor_add_del(self.pg8.sw_if_index, + mactobinary(self.pg8.remote_mac), + self.pg8.remote_ip4n, + is_static=1) + + self.vapi.ip_add_del_route(dst_address=self.pg7.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg7.remote_ip4n, + next_hop_sw_if_index=self.pg7.sw_if_index) + self.vapi.ip_add_del_route(dst_address=self.pg8.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg8.remote_ip4n, + next_hop_sw_if_index=self.pg8.sw_if_index) + + self.nat44_add_address(self.nat_addr) + self.nat44_add_static_mapping(self.pg7.remote_ip4, self.nat_addr, + self.tcp_port_in, self.tcp_port_out, + proto=IP_PROTOS.tcp) + self.nat44_add_static_mapping(self.pg7.remote_ip4, self.nat_addr, + self.udp_port_in, self.udp_port_out, + proto=IP_PROTOS.udp) + self.nat44_add_static_mapping(self.pg7.remote_ip4, self.nat_addr, + self.icmp_id_in, self.icmp_id_out, + proto=IP_PROTOS.icmp) + self.vapi.nat44_interface_add_del_feature(self.pg7.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg8.sw_if_index, + is_inside=0) + + # out2in + pkts = self.create_stream_out(self.pg8) + self.pg8.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg7.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg7) + + # in2out + pkts = self.create_stream_in(self.pg7, self.pg8) + self.pg7.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg8.get_capture(len(pkts)) + self.verify_capture_out(capture) + + def test_static_unknown_proto(self): + """ 1:1 NAT translate packet with unknown protocol """ + nat_ip = "10.0.0.10" + self.nat44_add_static_mapping(self.pg0.remote_ip4, nat_ip) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg3.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg1.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, nat_ip) + self.assertEqual(packet[IP].dst, self.pg1.remote_ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # out2in + p = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=nat_ip) / + GRE() / + IP(src=self.pg3.remote_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, self.pg1.remote_ip4) + self.assertEqual(packet[IP].dst, self.pg0.remote_ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_hairpinning_static_unknown_proto(self): + """ 1:1 NAT translate packet with unknown protocol - hairpinning """ + + host = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + + host_nat_ip = "10.0.0.10" + server_nat_ip = "10.0.0.11" + + self.nat44_add_static_mapping(host.ip4, host_nat_ip) + self.nat44_add_static_mapping(server.ip4, server_nat_ip) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # host to server + p = (Ether(dst=self.pg0.local_mac, src=host.mac) / + IP(src=host.ip4, dst=server_nat_ip) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg3.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, host_nat_ip) + self.assertEqual(packet[IP].dst, server.ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server to host + p = (Ether(dst=self.pg0.local_mac, src=server.mac) / + IP(src=server.ip4, dst=host_nat_ip) / + GRE() / + IP(src=self.pg3.remote_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, server_nat_ip) + self.assertEqual(packet[IP].dst, host.ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_unknown_proto(self): + """ NAT44 translate packet with unknown protocol """ + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=20)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg1.get_capture(1) + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg3.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg1.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, self.nat_addr) + self.assertEqual(packet[IP].dst, self.pg1.remote_ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # out2in + p = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + GRE() / + IP(src=self.pg3.remote_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, self.pg1.remote_ip4) + self.assertEqual(packet[IP].dst, self.pg0.remote_ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_hairpinning_unknown_proto(self): + """ NAT44 translate packet with unknown protocol - hairpinning """ + host = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + host_in_port = 1234 + host_out_port = 0 + server_in_port = 5678 + server_out_port = 8765 + server_nat_ip = "10.0.0.11" + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # add static mapping for server + self.nat44_add_static_mapping(server.ip4, server_nat_ip) + + # host to server + p = (Ether(src=host.mac, dst=self.pg0.local_mac) / + IP(src=host.ip4, dst=server_nat_ip) / + TCP(sport=host_in_port, dport=server_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + + p = (Ether(dst=self.pg0.local_mac, src=host.mac) / + IP(src=host.ip4, dst=server_nat_ip) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg3.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, self.nat_addr) + self.assertEqual(packet[IP].dst, server.ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server to host + p = (Ether(dst=self.pg0.local_mac, src=server.mac) / + IP(src=server.ip4, dst=self.nat_addr) / + GRE() / + IP(src=self.pg3.remote_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, server_nat_ip) + self.assertEqual(packet[IP].dst, host.ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_output_feature(self): + """ NAT44 interface output feature (in2out postrouting) """ + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_output_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_output_feature(self.pg1.sw_if_index) + self.vapi.nat44_interface_add_del_output_feature(self.pg3.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg3) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture) + + # out2in + pkts = self.create_stream_out(self.pg3) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + # from non-NAT interface to NAT inside interface + pkts = self.create_stream_in(self.pg2, self.pg0) + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_no_translation(capture, self.pg2, self.pg0) + + def test_output_feature_vrf_aware(self): + """ NAT44 interface output feature VRF aware (in2out postrouting) """ + nat_ip_vrf10 = "10.0.0.10" + nat_ip_vrf20 = "10.0.0.20" + + self.vapi.ip_add_del_route(dst_address=self.pg3.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg3.remote_ip4n, + next_hop_sw_if_index=self.pg3.sw_if_index, + table_id=10) + self.vapi.ip_add_del_route(dst_address=self.pg3.remote_ip4n, + dst_address_length=32, + next_hop_address=self.pg3.remote_ip4n, + next_hop_sw_if_index=self.pg3.sw_if_index, + table_id=20) + + self.nat44_add_address(nat_ip_vrf10, vrf_id=10) + self.nat44_add_address(nat_ip_vrf20, vrf_id=20) + self.vapi.nat44_interface_add_del_output_feature(self.pg4.sw_if_index) + self.vapi.nat44_interface_add_del_output_feature(self.pg6.sw_if_index) + self.vapi.nat44_interface_add_del_output_feature(self.pg3.sw_if_index, + is_inside=0) + + # in2out VRF 10 + pkts = self.create_stream_in(self.pg4, self.pg3) + self.pg4.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=nat_ip_vrf10) + + # out2in VRF 10 + pkts = self.create_stream_out(self.pg3, dst_ip=nat_ip_vrf10) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg4.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg4) + + # in2out VRF 20 + pkts = self.create_stream_in(self.pg6, self.pg3) + self.pg6.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg3.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=nat_ip_vrf20) + + # out2in VRF 20 + pkts = self.create_stream_out(self.pg3, dst_ip=nat_ip_vrf20) + self.pg3.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg6.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg6) + + def test_output_feature_hairpinning(self): + """ NAT44 interface output feature hairpinning (in2out postrouting) """ + host = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + host_in_port = 1234 + host_out_port = 0 + server_in_port = 5678 + server_out_port = 8765 + + self.nat44_add_address(self.nat_addr) + self.vapi.nat44_interface_add_del_output_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_output_feature(self.pg1.sw_if_index, + is_inside=0) + + # add static mapping for server + self.nat44_add_static_mapping(server.ip4, self.nat_addr, + server_in_port, server_out_port, + proto=IP_PROTOS.tcp) + + # send packet from host to server + p = (Ether(src=host.mac, dst=self.pg0.local_mac) / + IP(src=host.ip4, dst=self.nat_addr) / + TCP(sport=host_in_port, dport=server_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(ip.dst, server.ip4) + self.assertNotEqual(tcp.sport, host_in_port) + self.assertEqual(tcp.dport, server_in_port) + self.check_tcp_checksum(p) + host_out_port = tcp.sport + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # send reply from server to host + p = (Ether(src=server.mac, dst=self.pg0.local_mac) / + IP(src=server.ip4, dst=self.nat_addr) / + TCP(sport=server_in_port, dport=host_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.nat_addr) + self.assertEqual(ip.dst, host.ip4) + self.assertEqual(tcp.sport, server_out_port) + self.assertEqual(tcp.dport, host_in_port) + self.check_tcp_checksum(p) + except: + self.logger.error(ppp("Unexpected or invalid packet:"), p) + raise + + def tearDown(self): + super(TestNAT44, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show nat44 verbose")) + self.clear_nat44() + + +class TestDeterministicNAT(MethodHolder): + """ Deterministic NAT Test Cases """ + + @classmethod + def setUpConstants(cls): + super(TestDeterministicNAT, cls).setUpConstants() + cls.vpp_cmdline.extend(["nat", "{", "deterministic", "}"]) + + @classmethod + def setUpClass(cls): + super(TestDeterministicNAT, cls).setUpClass() + + try: + cls.tcp_port_in = 6303 + cls.tcp_external_port = 6303 + cls.udp_port_in = 6304 + cls.udp_external_port = 6304 + cls.icmp_id_in = 6305 + cls.nat_addr = '10.0.0.3' + + cls.create_pg_interfaces(range(3)) + cls.interfaces = list(cls.pg_interfaces) + + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + cls.pg0.generate_remote_hosts(2) + cls.pg0.configure_ipv4_neighbors() + + except Exception: + super(TestDeterministicNAT, cls).tearDownClass() + raise + + def create_stream_in(self, in_if, out_if, ttl=64): + """ + Create packet stream for inside network + + :param in_if: Inside interface + :param out_if: Outside interface + :param ttl: TTL of generated packets + """ + pkts = [] + # TCP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port)) + pkts.append(p) + + # UDP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + UDP(sport=self.udp_port_in, dport=self.udp_external_port)) + pkts.append(p) + + # ICMP + p = (Ether(dst=in_if.local_mac, src=in_if.remote_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=ttl) / + ICMP(id=self.icmp_id_in, type='echo-request')) + pkts.append(p) + + return pkts + + def create_stream_out(self, out_if, dst_ip=None, ttl=64): + """ + Create packet stream for outside network + + :param out_if: Outside interface + :param dst_ip: Destination IP address (Default use global NAT address) + :param ttl: TTL of generated packets + """ + if dst_ip is None: + dst_ip = self.nat_addr + pkts = [] + # TCP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + TCP(dport=self.tcp_port_out, sport=self.tcp_external_port)) + pkts.append(p) + + # UDP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + UDP(dport=self.udp_port_out, sport=self.udp_external_port)) + pkts.append(p) + + # ICMP + p = (Ether(dst=out_if.local_mac, src=out_if.remote_mac) / + IP(src=out_if.remote_ip4, dst=dst_ip, ttl=ttl) / + ICMP(id=self.icmp_external_id, type='echo-reply')) + pkts.append(p) + + return pkts + + def verify_capture_out(self, capture, nat_ip=None, packet_num=3): + """ + Verify captured packets on outside network + + :param capture: Captured packets + :param nat_ip: Translated IP address (Default use global NAT address) + :param same_port: Sorce port number is not translated (Default False) + :param packet_num: Expected number of packets (Default 3) + """ + if nat_ip is None: + nat_ip = self.nat_addr + self.assertEqual(packet_num, len(capture)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, nat_ip) + if packet.haslayer(TCP): + self.tcp_port_out = packet[TCP].sport + elif packet.haslayer(UDP): + self.udp_port_out = packet[UDP].sport + else: + self.icmp_external_id = packet[ICMP].id + except: + self.logger.error(ppp("Unexpected or invalid packet " + "(outside network):", packet)) + raise + + def initiate_tcp_session(self, in_if, out_if): + """ + Initiates TCP session + + :param in_if: Inside interface + :param out_if: Outside interface + """ + try: + # SYN packet in->out + p = (Ether(src=in_if.remote_mac, dst=in_if.local_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="S")) + in_if.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = out_if.get_capture(1) + p = capture[0] + self.tcp_port_out = p[TCP].sport + + # SYN + ACK packet out->in + p = (Ether(src=out_if.remote_mac, dst=out_if.local_mac) / + IP(src=out_if.remote_ip4, dst=self.nat_addr) / + TCP(sport=self.tcp_external_port, dport=self.tcp_port_out, + flags="SA")) + out_if.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + in_if.get_capture(1) + + # ACK packet in->out + p = (Ether(src=in_if.remote_mac, dst=in_if.local_mac) / + IP(src=in_if.remote_ip4, dst=out_if.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="A")) + in_if.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + out_if.get_capture(1) + + except: + self.logger.error("TCP 3 way handshake failed") + raise + + def verify_ipfix_max_entries_per_user(self, data): + """ + Verify IPFIX maximum entries per user exceeded event + + :param data: Decoded IPFIX data records + """ + self.assertEqual(1, len(data)) + record = data[0] + # natEvent + self.assertEqual(ord(record[230]), 13) + # natQuotaExceededEvent + self.assertEqual('\x03\x00\x00\x00', record[466]) + # sourceIPv4Address + self.assertEqual(self.pg0.remote_ip4n, record[8]) + + def test_deterministic_mode(self): + """ NAT plugin run deterministic mode """ + in_addr = '172.16.255.0' + out_addr = '172.17.255.50' + in_addr_t = '172.16.255.20' + in_addr_n = socket.inet_aton(in_addr) + out_addr_n = socket.inet_aton(out_addr) + in_addr_t_n = socket.inet_aton(in_addr_t) + in_plen = 24 + out_plen = 32 + + nat_config = self.vapi.nat_show_config() + self.assertEqual(1, nat_config.deterministic) + + self.vapi.nat_det_add_del_map(in_addr_n, in_plen, out_addr_n, out_plen) + + rep1 = self.vapi.nat_det_forward(in_addr_t_n) + self.assertEqual(rep1.out_addr[:4], out_addr_n) + rep2 = self.vapi.nat_det_reverse(out_addr_n, rep1.out_port_hi) + self.assertEqual(rep2.in_addr[:4], in_addr_t_n) + + deterministic_mappings = self.vapi.nat_det_map_dump() + self.assertEqual(len(deterministic_mappings), 1) + dsm = deterministic_mappings[0] + self.assertEqual(in_addr_n, dsm.in_addr[:4]) + self.assertEqual(in_plen, dsm.in_plen) + self.assertEqual(out_addr_n, dsm.out_addr[:4]) + self.assertEqual(out_plen, dsm.out_plen) + + self.clear_nat_det() + deterministic_mappings = self.vapi.nat_det_map_dump() + self.assertEqual(len(deterministic_mappings), 0) + + def test_set_timeouts(self): + """ Set deterministic NAT timeouts """ + timeouts_before = self.vapi.nat_det_get_timeouts() + + self.vapi.nat_det_set_timeouts(timeouts_before.udp + 10, + timeouts_before.tcp_established + 10, + timeouts_before.tcp_transitory + 10, + timeouts_before.icmp + 10) + + timeouts_after = self.vapi.nat_det_get_timeouts() + + self.assertNotEqual(timeouts_before.udp, timeouts_after.udp) + self.assertNotEqual(timeouts_before.icmp, timeouts_after.icmp) + self.assertNotEqual(timeouts_before.tcp_established, + timeouts_after.tcp_established) + self.assertNotEqual(timeouts_before.tcp_transitory, + timeouts_after.tcp_transitory) + + def test_det_in(self): + """ Deterministic NAT translation test (TCP, UDP, ICMP) """ + + nat_ip = "10.0.0.10" + + self.vapi.nat_det_add_del_map(self.pg0.remote_ip4n, + 32, + socket.inet_aton(nat_ip), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # in2out + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip) + + # out2in + pkts = self.create_stream_out(self.pg1, nat_ip) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in(capture, self.pg0) + + # session dump test + sessions = self.vapi.nat_det_session_dump(self.pg0.remote_ip4n) + self.assertEqual(len(sessions), 3) + + # TCP session + s = sessions[0] + self.assertEqual(s.ext_addr[:4], self.pg1.remote_ip4n) + self.assertEqual(s.in_port, self.tcp_port_in) + self.assertEqual(s.out_port, self.tcp_port_out) + self.assertEqual(s.ext_port, self.tcp_external_port) + + # UDP session + s = sessions[1] + self.assertEqual(s.ext_addr[:4], self.pg1.remote_ip4n) + self.assertEqual(s.in_port, self.udp_port_in) + self.assertEqual(s.out_port, self.udp_port_out) + self.assertEqual(s.ext_port, self.udp_external_port) + + # ICMP session + s = sessions[2] + self.assertEqual(s.ext_addr[:4], self.pg1.remote_ip4n) + self.assertEqual(s.in_port, self.icmp_id_in) + self.assertEqual(s.out_port, self.icmp_external_id) + + def test_multiple_users(self): + """ Deterministic NAT multiple users """ + + nat_ip = "10.0.0.10" + port_in = 80 + external_port = 6303 + + host0 = self.pg0.remote_hosts[0] + host1 = self.pg0.remote_hosts[1] + + self.vapi.nat_det_add_del_map(host0.ip4n, + 24, + socket.inet_aton(nat_ip), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + # host0 to out + p = (Ether(src=host0.mac, dst=self.pg0.local_mac) / + IP(src=host0.ip4, dst=self.pg1.remote_ip4) / + TCP(sport=port_in, dport=external_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, nat_ip) + self.assertEqual(ip.dst, self.pg1.remote_ip4) + self.assertEqual(tcp.dport, external_port) + port_out0 = tcp.sport + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # host1 to out + p = (Ether(src=host1.mac, dst=self.pg0.local_mac) / + IP(src=host1.ip4, dst=self.pg1.remote_ip4) / + TCP(sport=port_in, dport=external_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, nat_ip) + self.assertEqual(ip.dst, self.pg1.remote_ip4) + self.assertEqual(tcp.dport, external_port) + port_out1 = tcp.sport + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + dms = self.vapi.nat_det_map_dump() + self.assertEqual(1, len(dms)) + self.assertEqual(2, dms[0].ses_num) + + # out to host0 + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=nat_ip) / + TCP(sport=external_port, dport=port_out0)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.pg1.remote_ip4) + self.assertEqual(ip.dst, host0.ip4) + self.assertEqual(tcp.dport, port_in) + self.assertEqual(tcp.sport, external_port) + except: + self.logger.error(ppp("Unexpected or invalid packet:", p)) + raise + + # out to host1 + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=nat_ip) / + TCP(sport=external_port, dport=port_out1)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(1) + p = capture[0] + try: + ip = p[IP] + tcp = p[TCP] + self.assertEqual(ip.src, self.pg1.remote_ip4) + self.assertEqual(ip.dst, host1.ip4) + self.assertEqual(tcp.dport, port_in) + self.assertEqual(tcp.sport, external_port) + except: + self.logger.error(ppp("Unexpected or invalid packet", p)) + raise + + # session close api test + self.vapi.nat_det_close_session_out(socket.inet_aton(nat_ip), + port_out1, + self.pg1.remote_ip4n, + external_port) + dms = self.vapi.nat_det_map_dump() + self.assertEqual(dms[0].ses_num, 1) + + self.vapi.nat_det_close_session_in(host0.ip4n, + port_in, + self.pg1.remote_ip4n, + external_port) + dms = self.vapi.nat_det_map_dump() + self.assertEqual(dms[0].ses_num, 0) + + def test_tcp_session_close_detection_in(self): + """ Deterministic NAT TCP session close from inside network """ + self.vapi.nat_det_add_del_map(self.pg0.remote_ip4n, + 32, + socket.inet_aton(self.nat_addr), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + self.initiate_tcp_session(self.pg0, self.pg1) + + # close the session from inside + try: + # FIN packet in -> out + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="F")) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg1.get_capture(1) + + pkts = [] + + # ACK packet out -> in + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + TCP(sport=self.tcp_external_port, dport=self.tcp_port_out, + flags="A")) + pkts.append(p) + + # FIN packet out -> in + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + TCP(sport=self.tcp_external_port, dport=self.tcp_port_out, + flags="F")) + pkts.append(p) + + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.get_capture(2) + + # ACK packet in -> out + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="A")) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg1.get_capture(1) + + # Check if deterministic NAT44 closed the session + dms = self.vapi.nat_det_map_dump() + self.assertEqual(0, dms[0].ses_num) + except: + self.logger.error("TCP session termination failed") + raise + + def test_tcp_session_close_detection_out(self): + """ Deterministic NAT TCP session close from outside network """ + self.vapi.nat_det_add_del_map(self.pg0.remote_ip4n, + 32, + socket.inet_aton(self.nat_addr), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + self.initiate_tcp_session(self.pg0, self.pg1) + + # close the session from outside + try: + # FIN packet out -> in + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + TCP(sport=self.tcp_external_port, dport=self.tcp_port_out, + flags="F")) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.get_capture(1) + + pkts = [] + + # ACK packet in -> out + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="A")) + pkts.append(p) + + # ACK packet in -> out + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + TCP(sport=self.tcp_port_in, dport=self.tcp_external_port, + flags="F")) + pkts.append(p) + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg1.get_capture(2) + + # ACK packet out -> in + p = (Ether(src=self.pg1.remote_mac, dst=self.pg1.local_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + TCP(sport=self.tcp_external_port, dport=self.tcp_port_out, + flags="A")) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.pg0.get_capture(1) + + # Check if deterministic NAT44 closed the session + dms = self.vapi.nat_det_map_dump() + self.assertEqual(0, dms[0].ses_num) + except: + self.logger.error("TCP session termination failed") + raise + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_session_timeout(self): + """ Deterministic NAT session timeouts """ + self.vapi.nat_det_add_del_map(self.pg0.remote_ip4n, + 32, + socket.inet_aton(self.nat_addr), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + + self.initiate_tcp_session(self.pg0, self.pg1) + self.vapi.nat_det_set_timeouts(5, 5, 5, 5) + pkts = self.create_stream_in(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + sleep(15) + + dms = self.vapi.nat_det_map_dump() + self.assertEqual(0, dms[0].ses_num) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_session_limit_per_user(self): + """ Deterministic NAT maximum sessions per user limit """ + self.vapi.nat_det_add_del_map(self.pg0.remote_ip4n, + 32, + socket.inet_aton(self.nat_addr), + 32) + self.vapi.nat44_interface_add_del_feature(self.pg0.sw_if_index) + self.vapi.nat44_interface_add_del_feature(self.pg1.sw_if_index, + is_inside=0) + self.vapi.set_ipfix_exporter(collector_address=self.pg2.remote_ip4n, + src_address=self.pg2.local_ip4n, + path_mtu=512, + template_interval=10) + self.vapi.nat_ipfix() + + pkts = [] + for port in range(1025, 2025): + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(sport=port, dport=port)) + pkts.append(p) + + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + + p = (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(sport=3001, dport=3002)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.assert_nothing_captured() + + # verify ICMP error packet + capture = self.pg0.get_capture(1) + p = capture[0] + self.assertTrue(p.haslayer(ICMP)) + icmp = p[ICMP] + self.assertEqual(icmp.type, 3) + self.assertEqual(icmp.code, 1) + self.assertTrue(icmp.haslayer(IPerror)) + inner_ip = icmp[IPerror] + self.assertEqual(inner_ip[UDPerror].sport, 3001) + self.assertEqual(inner_ip[UDPerror].dport, 3002) + + dms = self.vapi.nat_det_map_dump() + + self.assertEqual(1000, dms[0].ses_num) + + # verify IPFIX logging + self.vapi.cli("ipfix flush") # FIXME this should be an API call + sleep(1) + capture = self.pg2.get_capture(2) + ipfix = IPFIXDecoder() + # first load template + for p in capture: + self.assertTrue(p.haslayer(IPFIX)) + if p.haslayer(Template): + ipfix.add_template(p.getlayer(Template)) + # verify events in data set + for p in capture: + if p.haslayer(Data): + data = ipfix.decode_data_set(p.getlayer(Set)) + self.verify_ipfix_max_entries_per_user(data) + + def clear_nat_det(self): + """ + Clear deterministic NAT configuration. + """ + self.vapi.nat_ipfix(enable=0) + self.vapi.nat_det_set_timeouts() + deterministic_mappings = self.vapi.nat_det_map_dump() + for dsm in deterministic_mappings: + self.vapi.nat_det_add_del_map(dsm.in_addr, + dsm.in_plen, + dsm.out_addr, + dsm.out_plen, + is_add=0) + + interfaces = self.vapi.nat44_interface_dump() + for intf in interfaces: + self.vapi.nat44_interface_add_del_feature(intf.sw_if_index, + intf.is_inside, + is_add=0) + + def tearDown(self): + super(TestDeterministicNAT, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show nat44 detail")) + self.clear_nat_det() + + +class TestNAT64(MethodHolder): + """ NAT64 Test Cases """ + + @classmethod + def setUpClass(cls): + super(TestNAT64, cls).setUpClass() + + try: + cls.tcp_port_in = 6303 + cls.tcp_port_out = 6303 + cls.udp_port_in = 6304 + cls.udp_port_out = 6304 + cls.icmp_id_in = 6305 + cls.icmp_id_out = 6305 + cls.nat_addr = '10.0.0.3' + cls.nat_addr_n = socket.inet_pton(socket.AF_INET, cls.nat_addr) + cls.vrf1_id = 10 + cls.vrf1_nat_addr = '10.0.10.3' + cls.vrf1_nat_addr_n = socket.inet_pton(socket.AF_INET, + cls.vrf1_nat_addr) + + cls.create_pg_interfaces(range(3)) + cls.ip6_interfaces = list(cls.pg_interfaces[0:1]) + cls.ip6_interfaces.append(cls.pg_interfaces[2]) + cls.ip4_interfaces = list(cls.pg_interfaces[1:2]) + + cls.vapi.ip_table_add_del(cls.vrf1_id, is_add=1, is_ipv6=1) + + cls.pg_interfaces[2].set_table_ip6(cls.vrf1_id) + + cls.pg0.generate_remote_hosts(2) + + for i in cls.ip6_interfaces: + i.admin_up() + i.config_ip6() + i.configure_ipv6_neighbors() + + for i in cls.ip4_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + except Exception: + super(TestNAT64, cls).tearDownClass() + raise + + def test_pool(self): + """ Add/delete address to NAT64 pool """ + nat_addr = socket.inet_pton(socket.AF_INET, '1.2.3.4') + + self.vapi.nat64_add_del_pool_addr_range(nat_addr, nat_addr) + + addresses = self.vapi.nat64_pool_addr_dump() + self.assertEqual(len(addresses), 1) + self.assertEqual(addresses[0].address, nat_addr) + + self.vapi.nat64_add_del_pool_addr_range(nat_addr, nat_addr, is_add=0) + + addresses = self.vapi.nat64_pool_addr_dump() + self.assertEqual(len(addresses), 0) + + def test_interface(self): + """ Enable/disable NAT64 feature on the interface """ + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + interfaces = self.vapi.nat64_interface_dump() + self.assertEqual(len(interfaces), 2) + pg0_found = False + pg1_found = False + for intf in interfaces: + if intf.sw_if_index == self.pg0.sw_if_index: + self.assertEqual(intf.is_inside, 1) + pg0_found = True + elif intf.sw_if_index == self.pg1.sw_if_index: + self.assertEqual(intf.is_inside, 0) + pg1_found = True + self.assertTrue(pg0_found) + self.assertTrue(pg1_found) + + features = self.vapi.cli("show interface features pg0") + self.assertNotEqual(features.find('nat64-in2out'), -1) + features = self.vapi.cli("show interface features pg1") + self.assertNotEqual(features.find('nat64-out2in'), -1) + + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index, is_add=0) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_add=0) + + interfaces = self.vapi.nat64_interface_dump() + self.assertEqual(len(interfaces), 0) + + def test_static_bib(self): + """ Add/delete static BIB entry """ + in_addr = socket.inet_pton(socket.AF_INET6, + '2001:db8:85a3::8a2e:370:7334') + out_addr = socket.inet_pton(socket.AF_INET, '10.1.1.3') + in_port = 1234 + out_port = 5678 + proto = IP_PROTOS.tcp + + self.vapi.nat64_add_del_static_bib(in_addr, + out_addr, + in_port, + out_port, + proto) + bib = self.vapi.nat64_bib_dump(IP_PROTOS.tcp) + static_bib_num = 0 + for bibe in bib: + if bibe.is_static: + static_bib_num += 1 + self.assertEqual(bibe.i_addr, in_addr) + self.assertEqual(bibe.o_addr, out_addr) + self.assertEqual(bibe.i_port, in_port) + self.assertEqual(bibe.o_port, out_port) + self.assertEqual(static_bib_num, 1) + + self.vapi.nat64_add_del_static_bib(in_addr, + out_addr, + in_port, + out_port, + proto, + is_add=0) + bib = self.vapi.nat64_bib_dump(IP_PROTOS.tcp) + static_bib_num = 0 + for bibe in bib: + if bibe.is_static: + static_bib_num += 1 + self.assertEqual(static_bib_num, 0) + + def test_set_timeouts(self): + """ Set NAT64 timeouts """ + # verify default values + timeouts = self.vapi.nat64_get_timeouts() + self.assertEqual(timeouts.udp, 300) + self.assertEqual(timeouts.icmp, 60) + self.assertEqual(timeouts.tcp_trans, 240) + self.assertEqual(timeouts.tcp_est, 7440) + self.assertEqual(timeouts.tcp_incoming_syn, 6) + + # set and verify custom values + self.vapi.nat64_set_timeouts(udp=200, icmp=30, tcp_trans=250, + tcp_est=7450, tcp_incoming_syn=10) + timeouts = self.vapi.nat64_get_timeouts() + self.assertEqual(timeouts.udp, 200) + self.assertEqual(timeouts.icmp, 30) + self.assertEqual(timeouts.tcp_trans, 250) + self.assertEqual(timeouts.tcp_est, 7450) + self.assertEqual(timeouts.tcp_incoming_syn, 10) + + def test_dynamic(self): + """ NAT64 dynamic translation test """ + self.tcp_port_in = 6303 + self.udp_port_in = 6304 + self.icmp_id_in = 6305 + + ses_num_start = self.nat64_get_ses_num() + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + # in2out + pkts = self.create_stream_in_ip6(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.nat_addr, + dst_ip=self.pg1.remote_ip4) + + # out2in + pkts = self.create_stream_out(self.pg1, dst_ip=self.nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + ip = IPv6(src=''.join(['64:ff9b::', self.pg1.remote_ip4])) + self.verify_capture_in_ip6(capture, ip[IPv6].src, self.pg0.remote_ip6) + + # in2out + pkts = self.create_stream_in_ip6(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.nat_addr, + dst_ip=self.pg1.remote_ip4) + + # out2in + pkts = self.create_stream_out(self.pg1, dst_ip=self.nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + self.verify_capture_in_ip6(capture, ip[IPv6].src, self.pg0.remote_ip6) + + ses_num_end = self.nat64_get_ses_num() + + self.assertEqual(ses_num_end - ses_num_start, 3) + + # tenant with specific VRF + self.vapi.nat64_add_del_pool_addr_range(self.vrf1_nat_addr_n, + self.vrf1_nat_addr_n, + vrf_id=self.vrf1_id) + self.vapi.nat64_add_del_interface(self.pg2.sw_if_index) + + pkts = self.create_stream_in_ip6(self.pg2, self.pg1) + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.vrf1_nat_addr, + dst_ip=self.pg1.remote_ip4) + + pkts = self.create_stream_out(self.pg1, dst_ip=self.vrf1_nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + self.verify_capture_in_ip6(capture, ip[IPv6].src, self.pg2.remote_ip6) + + def test_static(self): + """ NAT64 static translation test """ + self.tcp_port_in = 60303 + self.udp_port_in = 60304 + self.icmp_id_in = 60305 + self.tcp_port_out = 60303 + self.udp_port_out = 60304 + self.icmp_id_out = 60305 + + ses_num_start = self.nat64_get_ses_num() + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + self.vapi.nat64_add_del_static_bib(self.pg0.remote_ip6n, + self.nat_addr_n, + self.tcp_port_in, + self.tcp_port_out, + IP_PROTOS.tcp) + self.vapi.nat64_add_del_static_bib(self.pg0.remote_ip6n, + self.nat_addr_n, + self.udp_port_in, + self.udp_port_out, + IP_PROTOS.udp) + self.vapi.nat64_add_del_static_bib(self.pg0.remote_ip6n, + self.nat_addr_n, + self.icmp_id_in, + self.icmp_id_out, + IP_PROTOS.icmp) + + # in2out + pkts = self.create_stream_in_ip6(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.nat_addr, + dst_ip=self.pg1.remote_ip4, same_port=True) + + # out2in + pkts = self.create_stream_out(self.pg1, dst_ip=self.nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + ip = IPv6(src=''.join(['64:ff9b::', self.pg1.remote_ip4])) + self.verify_capture_in_ip6(capture, ip[IPv6].src, self.pg0.remote_ip6) + + ses_num_end = self.nat64_get_ses_num() + + self.assertEqual(ses_num_end - ses_num_start, 3) + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_session_timeout(self): + """ NAT64 session timeout """ + self.icmp_id_in = 1234 + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + self.vapi.nat64_set_timeouts(icmp=5) + + pkts = self.create_stream_in_ip6(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + + ses_num_before_timeout = self.nat64_get_ses_num() + + sleep(15) + + # ICMP session after timeout + ses_num_after_timeout = self.nat64_get_ses_num() + self.assertNotEqual(ses_num_before_timeout, ses_num_after_timeout) + + def test_icmp_error(self): + """ NAT64 ICMP Error message translation """ + self.tcp_port_in = 6303 + self.udp_port_in = 6304 + self.icmp_id_in = 6305 + + ses_num_start = self.nat64_get_ses_num() + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + # send some packets to create sessions + pkts = self.create_stream_in_ip6(self.pg0, self.pg1) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture_ip4 = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture_ip4, + nat_ip=self.nat_addr, + dst_ip=self.pg1.remote_ip4) + + pkts = self.create_stream_out(self.pg1, dst_ip=self.nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture_ip6 = self.pg0.get_capture(len(pkts)) + ip = IPv6(src=''.join(['64:ff9b::', self.pg1.remote_ip4])) + self.verify_capture_in_ip6(capture_ip6, ip[IPv6].src, + self.pg0.remote_ip6) + + # in2out + pkts = [Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=self.pg0.remote_ip6, dst=ip[IPv6].src) / + ICMPv6DestUnreach(code=1) / + packet[IPv6] for packet in capture_ip6] + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IP].src, self.nat_addr) + self.assertEqual(packet[IP].dst, self.pg1.remote_ip4) + self.assertEqual(packet[ICMP].type, 3) + self.assertEqual(packet[ICMP].code, 13) + inner = packet[IPerror] + self.assertEqual(inner.src, self.pg1.remote_ip4) + self.assertEqual(inner.dst, self.nat_addr) + self.check_icmp_checksum(packet) + if inner.haslayer(TCPerror): + self.assertEqual(inner[TCPerror].dport, self.tcp_port_out) + elif inner.haslayer(UDPerror): + self.assertEqual(inner[UDPerror].dport, self.udp_port_out) + else: + self.assertEqual(inner[ICMPerror].id, self.icmp_id_out) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # out2in + pkts = [Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + ICMP(type=3, code=13) / + packet[IP] for packet in capture_ip4] + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IPv6].src, ip.src) + self.assertEqual(packet[IPv6].dst, self.pg0.remote_ip6) + icmp = packet[ICMPv6DestUnreach] + self.assertEqual(icmp.code, 1) + inner = icmp[IPerror6] + self.assertEqual(inner.src, self.pg0.remote_ip6) + self.assertEqual(inner.dst, ip.src) + self.check_icmpv6_checksum(packet) + if inner.haslayer(TCPerror): + self.assertEqual(inner[TCPerror].sport, self.tcp_port_in) + elif inner.haslayer(UDPerror): + self.assertEqual(inner[UDPerror].sport, self.udp_port_in) + else: + self.assertEqual(inner[ICMPv6EchoRequest].id, + self.icmp_id_in) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_hairpinning(self): + """ NAT64 hairpinning """ + + client = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + server_tcp_in_port = 22 + server_tcp_out_port = 4022 + server_udp_in_port = 23 + server_udp_out_port = 4023 + client_tcp_in_port = 1234 + client_udp_in_port = 1235 + client_tcp_out_port = 0 + client_udp_out_port = 0 + ip = IPv6(src=''.join(['64:ff9b::', self.nat_addr])) + nat_addr_ip6 = ip.src + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + self.vapi.nat64_add_del_static_bib(server.ip6n, + self.nat_addr_n, + server_tcp_in_port, + server_tcp_out_port, + IP_PROTOS.tcp) + self.vapi.nat64_add_del_static_bib(server.ip6n, + self.nat_addr_n, + server_udp_in_port, + server_udp_out_port, + IP_PROTOS.udp) + + # client to server + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=client.ip6, dst=nat_addr_ip6) / + TCP(sport=client_tcp_in_port, dport=server_tcp_out_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=client.ip6, dst=nat_addr_ip6) / + UDP(sport=client_udp_in_port, dport=server_udp_out_port)) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IPv6].src, nat_addr_ip6) + self.assertEqual(packet[IPv6].dst, server.ip6) + if packet.haslayer(TCP): + self.assertNotEqual(packet[TCP].sport, client_tcp_in_port) + self.assertEqual(packet[TCP].dport, server_tcp_in_port) + self.check_tcp_checksum(packet) + client_tcp_out_port = packet[TCP].sport + else: + self.assertNotEqual(packet[UDP].sport, client_udp_in_port) + self.assertEqual(packet[UDP].dport, server_udp_in_port) + self.check_udp_checksum(packet) + client_udp_out_port = packet[UDP].sport + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server to client + pkts = [] + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=server.ip6, dst=nat_addr_ip6) / + TCP(sport=server_tcp_in_port, dport=client_tcp_out_port)) + pkts.append(p) + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=server.ip6, dst=nat_addr_ip6) / + UDP(sport=server_udp_in_port, dport=client_udp_out_port)) + pkts.append(p) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IPv6].src, nat_addr_ip6) + self.assertEqual(packet[IPv6].dst, client.ip6) + if packet.haslayer(TCP): + self.assertEqual(packet[TCP].sport, server_tcp_out_port) + self.assertEqual(packet[TCP].dport, client_tcp_in_port) + self.check_tcp_checksum(packet) + else: + self.assertEqual(packet[UDP].sport, server_udp_out_port) + self.assertEqual(packet[UDP].dport, client_udp_in_port) + self.check_udp_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # ICMP error + pkts = [] + pkts = [Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=client.ip6, dst=nat_addr_ip6) / + ICMPv6DestUnreach(code=1) / + packet[IPv6] for packet in capture] + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + for packet in capture: + try: + self.assertEqual(packet[IPv6].src, nat_addr_ip6) + self.assertEqual(packet[IPv6].dst, server.ip6) + icmp = packet[ICMPv6DestUnreach] + self.assertEqual(icmp.code, 1) + inner = icmp[IPerror6] + self.assertEqual(inner.src, server.ip6) + self.assertEqual(inner.dst, nat_addr_ip6) + self.check_icmpv6_checksum(packet) + if inner.haslayer(TCPerror): + self.assertEqual(inner[TCPerror].sport, server_tcp_in_port) + self.assertEqual(inner[TCPerror].dport, + client_tcp_out_port) + else: + self.assertEqual(inner[UDPerror].sport, server_udp_in_port) + self.assertEqual(inner[UDPerror].dport, + client_udp_out_port) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_prefix(self): + """ NAT64 Network-Specific Prefix """ + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + self.vapi.nat64_add_del_pool_addr_range(self.vrf1_nat_addr_n, + self.vrf1_nat_addr_n, + vrf_id=self.vrf1_id) + self.vapi.nat64_add_del_interface(self.pg2.sw_if_index) + + # Add global prefix + global_pref64 = "2001:db8::" + global_pref64_n = socket.inet_pton(socket.AF_INET6, global_pref64) + global_pref64_len = 32 + self.vapi.nat64_add_del_prefix(global_pref64_n, global_pref64_len) + + prefix = self.vapi.nat64_prefix_dump() + self.assertEqual(len(prefix), 1) + self.assertEqual(prefix[0].prefix, global_pref64_n) + self.assertEqual(prefix[0].prefix_len, global_pref64_len) + self.assertEqual(prefix[0].vrf_id, 0) + + # Add tenant specific prefix + vrf1_pref64 = "2001:db8:122:300::" + vrf1_pref64_n = socket.inet_pton(socket.AF_INET6, vrf1_pref64) + vrf1_pref64_len = 56 + self.vapi.nat64_add_del_prefix(vrf1_pref64_n, + vrf1_pref64_len, + vrf_id=self.vrf1_id) + prefix = self.vapi.nat64_prefix_dump() + self.assertEqual(len(prefix), 2) + + # Global prefix + pkts = self.create_stream_in_ip6(self.pg0, + self.pg1, + pref=global_pref64, + plen=global_pref64_len) + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.nat_addr, + dst_ip=self.pg1.remote_ip4) + + pkts = self.create_stream_out(self.pg1, dst_ip=self.nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg0.get_capture(len(pkts)) + dst_ip = self.compose_ip6(self.pg1.remote_ip4, + global_pref64, + global_pref64_len) + self.verify_capture_in_ip6(capture, dst_ip, self.pg0.remote_ip6) + + # Tenant specific prefix + pkts = self.create_stream_in_ip6(self.pg2, + self.pg1, + pref=vrf1_pref64, + plen=vrf1_pref64_len) + self.pg2.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg1.get_capture(len(pkts)) + self.verify_capture_out(capture, nat_ip=self.vrf1_nat_addr, + dst_ip=self.pg1.remote_ip4) + + pkts = self.create_stream_out(self.pg1, dst_ip=self.vrf1_nat_addr) + self.pg1.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + capture = self.pg2.get_capture(len(pkts)) + dst_ip = self.compose_ip6(self.pg1.remote_ip4, + vrf1_pref64, + vrf1_pref64_len) + self.verify_capture_in_ip6(capture, dst_ip, self.pg2.remote_ip6) + + def test_unknown_proto(self): + """ NAT64 translate packet with unknown protocol """ + + self.vapi.nat64_add_del_pool_addr_range(self.nat_addr_n, + self.nat_addr_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + remote_ip6 = self.compose_ip6(self.pg1.remote_ip4, '64:ff9b::', 96) + + # in2out + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=self.pg0.remote_ip6, dst=remote_ip6) / + TCP(sport=self.tcp_port_in, dport=20)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg1.get_capture(1) + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=self.pg0.remote_ip6, dst=remote_ip6, nh=47) / + GRE() / + IP(src=self.pg2.local_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg1.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IP].src, self.nat_addr) + self.assertEqual(packet[IP].dst, self.pg1.remote_ip4) + self.assertTrue(packet.haslayer(GRE)) + self.check_ip_checksum(packet) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # out2in + p = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + IP(src=self.pg1.remote_ip4, dst=self.nat_addr) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg2.local_ip4) / + TCP(sport=1234, dport=1234)) + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IPv6].src, remote_ip6) + self.assertEqual(packet[IPv6].dst, self.pg0.remote_ip6) + self.assertEqual(packet[IPv6].nh, 47) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def test_hairpinning_unknown_proto(self): + """ NAT64 translate packet with unknown protocol - hairpinning """ + + client = self.pg0.remote_hosts[0] + server = self.pg0.remote_hosts[1] + server_tcp_in_port = 22 + server_tcp_out_port = 4022 + client_tcp_in_port = 1234 + client_tcp_out_port = 1235 + server_nat_ip = "10.0.0.100" + client_nat_ip = "10.0.0.110" + server_nat_ip_n = socket.inet_pton(socket.AF_INET, server_nat_ip) + client_nat_ip_n = socket.inet_pton(socket.AF_INET, client_nat_ip) + server_nat_ip6 = self.compose_ip6(server_nat_ip, '64:ff9b::', 96) + client_nat_ip6 = self.compose_ip6(client_nat_ip, '64:ff9b::', 96) + + self.vapi.nat64_add_del_pool_addr_range(server_nat_ip_n, + client_nat_ip_n) + self.vapi.nat64_add_del_interface(self.pg0.sw_if_index) + self.vapi.nat64_add_del_interface(self.pg1.sw_if_index, is_inside=0) + + self.vapi.nat64_add_del_static_bib(server.ip6n, + server_nat_ip_n, + server_tcp_in_port, + server_tcp_out_port, + IP_PROTOS.tcp) + + self.vapi.nat64_add_del_static_bib(server.ip6n, + server_nat_ip_n, + 0, + 0, + IP_PROTOS.gre) + + self.vapi.nat64_add_del_static_bib(client.ip6n, + client_nat_ip_n, + client_tcp_in_port, + client_tcp_out_port, + IP_PROTOS.tcp) + + # client to server + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=client.ip6, dst=server_nat_ip6) / + TCP(sport=client_tcp_in_port, dport=server_tcp_out_port)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=client.ip6, dst=server_nat_ip6, nh=IP_PROTOS.gre) / + GRE() / + IP(src=self.pg2.local_ip4, dst=self.pg2.remote_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IPv6].src, client_nat_ip6) + self.assertEqual(packet[IPv6].dst, server.ip6) + self.assertEqual(packet[IPv6].nh, IP_PROTOS.gre) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # server to client + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IPv6(src=server.ip6, dst=client_nat_ip6, nh=IP_PROTOS.gre) / + GRE() / + IP(src=self.pg2.remote_ip4, dst=self.pg2.local_ip4) / + TCP(sport=1234, dport=1234)) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + p = self.pg0.get_capture(1) + packet = p[0] + try: + self.assertEqual(packet[IPv6].src, server_nat_ip6) + self.assertEqual(packet[IPv6].dst, client.ip6) + self.assertEqual(packet[IPv6].nh, IP_PROTOS.gre) + except: + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + def nat64_get_ses_num(self): + """ + Return number of active NAT64 sessions. + """ + st = self.vapi.nat64_st_dump() + return len(st) + + def clear_nat64(self): + """ + Clear NAT64 configuration. + """ + self.vapi.nat64_set_timeouts() + + interfaces = self.vapi.nat64_interface_dump() + for intf in interfaces: + self.vapi.nat64_add_del_interface(intf.sw_if_index, + intf.is_inside, + is_add=0) + + bib = self.vapi.nat64_bib_dump(IP_PROTOS.tcp) + for bibe in bib: + if bibe.is_static: + self.vapi.nat64_add_del_static_bib(bibe.i_addr, + bibe.o_addr, + bibe.i_port, + bibe.o_port, + bibe.proto, + bibe.vrf_id, + is_add=0) + + bib = self.vapi.nat64_bib_dump(IP_PROTOS.udp) + for bibe in bib: + if bibe.is_static: + self.vapi.nat64_add_del_static_bib(bibe.i_addr, + bibe.o_addr, + bibe.i_port, + bibe.o_port, + bibe.proto, + bibe.vrf_id, + is_add=0) + + bib = self.vapi.nat64_bib_dump(IP_PROTOS.icmp) + for bibe in bib: + if bibe.is_static: + self.vapi.nat64_add_del_static_bib(bibe.i_addr, + bibe.o_addr, + bibe.i_port, + bibe.o_port, + bibe.proto, + bibe.vrf_id, + is_add=0) + + adresses = self.vapi.nat64_pool_addr_dump() + for addr in adresses: + self.vapi.nat64_add_del_pool_addr_range(addr.address, + addr.address, + vrf_id=addr.vrf_id, + is_add=0) + + prefixes = self.vapi.nat64_prefix_dump() + for prefix in prefixes: + self.vapi.nat64_add_del_prefix(prefix.prefix, + prefix.prefix_len, + vrf_id=prefix.vrf_id, + is_add=0) + + def tearDown(self): + super(TestNAT64, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show nat64 pool")) + self.logger.info(self.vapi.cli("show nat64 interfaces")) + self.logger.info(self.vapi.cli("show nat64 prefix")) + self.logger.info(self.vapi.cli("show nat64 bib all")) + self.logger.info(self.vapi.cli("show nat64 session table all")) + self.clear_nat64() + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_neighbor.py b/test/test_neighbor.py new file mode 100644 index 00000000..68dde2fb --- /dev/null +++ b/test/test_neighbor.py @@ -0,0 +1,1147 @@ +#!/usr/bin/env python + +import unittest +from socket import AF_INET, AF_INET6, inet_pton + +from framework import VppTestCase, VppTestRunner +from vpp_neighbor import VppNeighbor, find_nbr +from vpp_ip_route import VppIpRoute, VppRoutePath, find_route, \ + VppIpTable + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, ARP, Dot1Q +from scapy.layers.inet import IP, UDP +from scapy.contrib.mpls import MPLS + +# not exported by scapy, so redefined here +arp_opts = {"who-has": 1, "is-at": 2} + + +class ARPTestCase(VppTestCase): + """ ARP Test Case """ + + def setUp(self): + super(ARPTestCase, self).setUp() + + # create 3 pg interfaces + self.create_pg_interfaces(range(4)) + + # pg0 configured with ip4 and 6 addresses used for input + # pg1 configured with ip4 and 6 addresses used for output + # pg2 is unnumbered to pg0 + for i in self.pg_interfaces: + i.admin_up() + + self.pg0.config_ip4() + self.pg0.config_ip6() + self.pg0.resolve_arp() + + self.pg1.config_ip4() + self.pg1.config_ip6() + + # pg3 in a different VRF + self.tbl = VppIpTable(self, 1) + self.tbl.add_vpp_config() + + self.pg3.set_table_ip4(1) + self.pg3.config_ip4() + + def tearDown(self): + self.pg0.unconfig_ip4() + self.pg0.unconfig_ip6() + + self.pg1.unconfig_ip4() + self.pg1.unconfig_ip6() + + self.pg3.unconfig_ip4() + self.pg3.set_table_ip4(0) + + for i in self.pg_interfaces: + i.admin_down() + + super(ARPTestCase, self).tearDown() + + def verify_arp_req(self, rx, smac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff") + self.assertEqual(ether.src, smac) + + arp = rx[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + self.assertEqual(arp.op, arp_opts["who-has"]) + self.assertEqual(arp.hwsrc, smac) + self.assertEqual(arp.hwdst, "00:00:00:00:00:00") + self.assertEqual(arp.psrc, sip) + self.assertEqual(arp.pdst, dip) + + def verify_arp_resp(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + arp = rx[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + self.assertEqual(arp.op, arp_opts["is-at"]) + self.assertEqual(arp.hwsrc, smac) + self.assertEqual(arp.hwdst, dmac) + self.assertEqual(arp.psrc, sip) + self.assertEqual(arp.pdst, dip) + + def verify_arp_vrrp_resp(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + arp = rx[ARP] + self.assertEqual(arp.hwtype, 1) + self.assertEqual(arp.ptype, 0x800) + self.assertEqual(arp.hwlen, 6) + self.assertEqual(arp.plen, 4) + self.assertEqual(arp.op, arp_opts["is-at"]) + self.assertNotEqual(arp.hwsrc, smac) + self.assertTrue("00:00:5e:00:01" in arp.hwsrc or + "00:00:5E:00:01" in arp.hwsrc) + self.assertEqual(arp.hwdst, dmac) + self.assertEqual(arp.psrc, sip) + self.assertEqual(arp.pdst, dip) + + def verify_ip(self, rx, smac, dmac, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + ip = rx[IP] + self.assertEqual(ip.src, sip) + self.assertEqual(ip.dst, dip) + + def verify_ip_o_mpls(self, rx, smac, dmac, label, sip, dip): + ether = rx[Ether] + self.assertEqual(ether.dst, dmac) + self.assertEqual(ether.src, smac) + + mpls = rx[MPLS] + self.assertTrue(mpls.label, label) + + ip = rx[IP] + self.assertEqual(ip.src, sip) + self.assertEqual(ip.dst, dip) + + def send_and_assert_no_replies(self, intf, pkts, remark): + intf.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + timeout = 1 + for i in self.pg_interfaces: + i.get_capture(0, timeout=timeout) + i.assert_nothing_captured(remark=remark) + timeout = 0.1 + + def test_arp(self): + """ ARP """ + + # + # Generate some hosts on the LAN + # + self.pg1.generate_remote_hosts(11) + + # + # Send IP traffic to one of these unresolved hosts. + # expect the generation of an ARP request + # + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1._remote_hosts[1].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_arp_req(rx[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # And a dynamic ARP entry for host 1 + # + dyn_arp = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip4) + dyn_arp.add_vpp_config() + + # + # now we expect IP traffic forwarded + # + dyn_p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[1].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(dyn_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # And a Static ARP entry for host 2 + # + static_arp = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[2].mac, + self.pg1.remote_hosts[2].ip4, + is_static=1) + static_arp.add_vpp_config() + + static_p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[2].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(static_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[2].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[2].ip4) + + # + # flap the link. dynamic ARPs get flush, statics don't + # + self.pg1.admin_down() + self.pg1.admin_up() + + self.pg0.add_stream(static_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + + self.verify_ip(rx[0], + self.pg1.local_mac, + self.pg1.remote_hosts[2].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[2].ip4) + + self.pg0.add_stream(dyn_p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_req(rx[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[1].ip4) + + # + # Send an ARP request from one of the so-far unlearned remote hosts + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg1._remote_hosts[3].mac) / + ARP(op="who-has", + hwsrc=self.pg1._remote_hosts[3].mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1._remote_hosts[3].ip4)) + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1._remote_hosts[3].mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[3].ip4) + + # + # VPP should have learned the mapping for the remote host + # + self.assertTrue(find_nbr(self, + self.pg1.sw_if_index, + self.pg1._remote_hosts[3].ip4)) + # + # Fire in an ARP request before the interface becomes IP enabled + # + self.pg2.generate_remote_hosts(4) + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg2.remote_hosts[3].ip4)) + pt = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + Dot1Q(vlan=0) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg2.remote_hosts[3].ip4)) + self.send_and_assert_no_replies(self.pg2, p, + "interface not IP enabled") + + # + # Make pg2 un-numbered to pg1 + # + self.pg2.set_unnumbered(self.pg1.sw_if_index) + + # + # We should respond to ARP requests for the unnumbered to address + # once an attached route to the source is known + # + self.send_and_assert_no_replies( + self.pg2, p, + "ARP req for unnumbered address - no source") + + attached_host = VppIpRoute(self, self.pg2.remote_hosts[3].ip4, 32, + [VppRoutePath("0.0.0.0", + self.pg2.sw_if_index)]) + attached_host.add_vpp_config() + + self.pg2.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg1.local_ip4, + self.pg2.remote_hosts[3].ip4) + + self.pg2.add_stream(pt) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg1.local_ip4, + self.pg2.remote_hosts[3].ip4) + + # + # A neighbor entry that has no associated FIB-entry + # + arp_no_fib = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[4].mac, + self.pg1.remote_hosts[4].ip4, + is_no_fib_entry=1) + arp_no_fib.add_vpp_config() + + # + # check we have the neighbor, but no route + # + self.assertTrue(find_nbr(self, + self.pg1.sw_if_index, + self.pg1._remote_hosts[4].ip4)) + self.assertFalse(find_route(self, + self.pg1._remote_hosts[4].ip4, + 32)) + # + # pg2 is unnumbered to pg1, so we can form adjacencies out of pg2 + # from within pg1's subnet + # + arp_unnum = VppNeighbor(self, + self.pg2.sw_if_index, + self.pg1.remote_hosts[5].mac, + self.pg1.remote_hosts[5].ip4) + arp_unnum.add_vpp_config() + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[5].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + + self.verify_ip(rx[0], + self.pg2.local_mac, + self.pg1.remote_hosts[5].mac, + self.pg0.remote_ip4, + self.pg1._remote_hosts[5].ip4) + + # + # ARP requests from hosts in pg1's subnet sent on pg2 are replied to + # with the unnumbered interface's address as the source + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_hosts[6].ip4)) + + self.pg2.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[6].ip4) + + # + # An attached host route out of pg2 for an undiscovered hosts generates + # an ARP request with the unnumbered address as the source + # + att_unnum = VppIpRoute(self, self.pg1.remote_hosts[7].ip4, 32, + [VppRoutePath("0.0.0.0", + self.pg2.sw_if_index)]) + att_unnum.add_vpp_config() + + p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1._remote_hosts[7].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + + self.verify_arp_req(rx[0], + self.pg2.local_mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[7].ip4) + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_hosts[7].ip4)) + + self.pg2.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[7].ip4) + + # + # An attached host route as yet unresolved out of pg2 for an + # undiscovered host, an ARP requests begets a response. + # + att_unnum1 = VppIpRoute(self, self.pg1.remote_hosts[8].ip4, 32, + [VppRoutePath("0.0.0.0", + self.pg2.sw_if_index)]) + att_unnum1.add_vpp_config() + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_hosts[8].ip4)) + + self.pg2.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[8].ip4) + + # + # Send an ARP request from one of the so-far unlearned remote hosts + # with a VLAN0 tag + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", + src=self.pg1._remote_hosts[9].mac) / + Dot1Q(vlan=0) / + ARP(op="who-has", + hwsrc=self.pg1._remote_hosts[9].mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1._remote_hosts[9].ip4)) + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1._remote_hosts[9].mac, + self.pg1.local_ip4, + self.pg1._remote_hosts[9].ip4) + + # + # Add a hierachy of routes for a host in the sub-net. + # Should still get an ARP resp since the cover is attached + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg1.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_hosts[10].ip4)) + + r1 = VppIpRoute(self, self.pg1.remote_hosts[10].ip4, 30, + [VppRoutePath(self.pg1.remote_hosts[10].ip4, + self.pg1.sw_if_index)]) + r1.add_vpp_config() + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1.remote_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[10].ip4) + + r2 = VppIpRoute(self, self.pg1.remote_hosts[10].ip4, 32, + [VppRoutePath(self.pg1.remote_hosts[10].ip4, + self.pg1.sw_if_index)]) + r2.add_vpp_config() + + self.pg1.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1.remote_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[10].ip4) + + # + # add an ARP entry that's not on the sub-net and so whose + # adj-fib fails the refinement check. then send an ARP request + # from that source + # + a1 = VppNeighbor(self, + self.pg0.sw_if_index, + self.pg0.remote_mac, + "100.100.100.50") + a1.add_vpp_config() + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc="100.100.100.50", + pdst=self.pg0.remote_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for from failed adj-fib") + + # + # ERROR Cases + # 1 - don't respond to ARP request for address not within the + # interface's sub-net + # 1b - nor within the unnumbered subnet + # 1c - nor within the subnet of a different interface + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst="10.10.10.3", + psrc=self.pg0.remote_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local destination") + self.assertFalse(find_nbr(self, + self.pg0.sw_if_index, + "10.10.10.3")) + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst="10.10.10.3", + psrc=self.pg1.remote_hosts[7].ip4)) + self.send_and_assert_no_replies( + self.pg0, p, + "ARP req for non-local destination - unnum") + + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req diff sub-net") + self.assertFalse(find_nbr(self, + self.pg0.sw_if_index, + self.pg1.remote_ip4)) + + # + # 2 - don't respond to ARP request from an address not within the + # interface's sub-net + # 2b - to a prxied address + # 2c - not within a differents interface's sub-net + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc="10.10.10.3", + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg2.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + psrc="10.10.10.3", + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies( + self.pg0, p, + "ARP req for non-local source - unnum") + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc=self.pg1.remote_ip4, + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source 2c") + + # + # 3 - don't respond to ARP request from an address that belongs to + # the router + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + psrc=self.pg0.local_ip4, + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + + # + # 4 - don't respond to ARP requests that has mac source different + # from ARP request HW source + # the router + # + p = (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.pg0.remote_mac) / + ARP(op="who-has", + hwsrc="00:00:00:DE:AD:BE", + psrc=self.pg0.remote_ip4, + pdst=self.pg0.local_ip4)) + self.send_and_assert_no_replies(self.pg0, p, + "ARP req for non-local source") + + # + # cleanup + # + dyn_arp.remove_vpp_config() + static_arp.remove_vpp_config() + self.pg2.unset_unnumbered(self.pg1.sw_if_index) + + # need this to flush the adj-fibs + self.pg2.unset_unnumbered(self.pg1.sw_if_index) + self.pg2.admin_down() + self.pg1.admin_down() + + def test_proxy_mirror_arp(self): + """ Interface Mirror Proxy ARP """ + + # + # When VPP has an interface whose address is also applied to a TAP + # interface on the host, then VPP's TAP interface will be unnumbered + # to the 'real' interface and do proxy ARP from the host. + # the curious aspect of this setup is that ARP requests from the host + # will come from the VPP's own address. + # + self.pg0.generate_remote_hosts(2) + + arp_req_from_me = (Ether(src=self.pg2.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst=self.pg0.remote_hosts[1].ip4, + psrc=self.pg0.local_ip4)) + + # + # Configure Proxy ARP for the subnet on PG0addresses on pg0 + # + self.vapi.proxy_arp_add_del(self.pg0._local_ip4n_subnet, + self.pg0._local_ip4n_bcast) + + # Make pg2 un-numbered to pg0 + # + self.pg2.set_unnumbered(self.pg0.sw_if_index) + + # + # Enable pg2 for proxy ARP + # + self.pg2.set_proxy_arp() + + # + # Send the ARP request with an originating address that + # is VPP's own address + # + self.pg2.add_stream(arp_req_from_me) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + self.pg0.remote_hosts[1].ip4, + self.pg0.local_ip4) + + # + # validate we have not learned an ARP entry as a result of this + # + self.assertFalse(find_nbr(self, + self.pg2.sw_if_index, + self.pg0.local_ip4)) + + # + # cleanup + # + self.pg2.set_proxy_arp(0) + self.vapi.proxy_arp_add_del(self.pg0._local_ip4n_subnet, + self.pg0._local_ip4n_bcast, + is_add=0) + + def test_proxy_arp(self): + """ Proxy ARP """ + + self.pg1.generate_remote_hosts(2) + + # + # Proxy ARP rewquest packets for each interface + # + arp_req_pg0 = (Ether(src=self.pg0.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst="10.10.10.3", + psrc=self.pg0.remote_ip4)) + arp_req_pg0_tagged = (Ether(src=self.pg0.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + Dot1Q(vlan=0) / + ARP(op="who-has", + hwsrc=self.pg0.remote_mac, + pdst="10.10.10.3", + psrc=self.pg0.remote_ip4)) + arp_req_pg1 = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.3", + psrc=self.pg1.remote_ip4)) + arp_req_pg2 = (Ether(src=self.pg2.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg2.remote_mac, + pdst="10.10.10.3", + psrc=self.pg1.remote_hosts[1].ip4)) + arp_req_pg3 = (Ether(src=self.pg3.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg3.remote_mac, + pdst="10.10.10.3", + psrc=self.pg3.remote_ip4)) + + # + # Configure Proxy ARP for 10.10.10.0 -> 10.10.10.124 + # + self.vapi.proxy_arp_add_del(inet_pton(AF_INET, "10.10.10.2"), + inet_pton(AF_INET, "10.10.10.124")) + + # + # No responses are sent when the interfaces are not enabled for proxy + # ARP + # + self.send_and_assert_no_replies(self.pg0, arp_req_pg0, + "ARP req from unconfigured interface") + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from unconfigured interface") + + # + # Make pg2 un-numbered to pg1 + # still won't reply. + # + self.pg2.set_unnumbered(self.pg1.sw_if_index) + + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from unnumbered interface") + + # + # Enable each interface to reply to proxy ARPs + # + for i in self.pg_interfaces: + i.set_proxy_arp() + + # + # Now each of the interfaces should reply to a request to a proxied + # address + # + self.pg0.add_stream(arp_req_pg0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg0.local_mac, + self.pg0.remote_mac, + "10.10.10.3", + self.pg0.remote_ip4) + + self.pg0.add_stream(arp_req_pg0_tagged) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg0.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg0.local_mac, + self.pg0.remote_mac, + "10.10.10.3", + self.pg0.remote_ip4) + + self.pg1.add_stream(arp_req_pg1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg1.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg1.local_mac, + self.pg1.remote_mac, + "10.10.10.3", + self.pg1.remote_ip4) + + self.pg2.add_stream(arp_req_pg2) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_resp(rx[0], + self.pg2.local_mac, + self.pg2.remote_mac, + "10.10.10.3", + self.pg1.remote_hosts[1].ip4) + + # + # A request for an address out of the configured range + # + arp_req_pg1_hi = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.125", + psrc=self.pg1.remote_ip4)) + self.send_and_assert_no_replies(self.pg1, arp_req_pg1_hi, + "ARP req out of range HI") + arp_req_pg1_low = (Ether(src=self.pg1.remote_mac, + dst="ff:ff:ff:ff:ff:ff") / + ARP(op="who-has", + hwsrc=self.pg1.remote_mac, + pdst="10.10.10.1", + psrc=self.pg1.remote_ip4)) + self.send_and_assert_no_replies(self.pg1, arp_req_pg1_low, + "ARP req out of range Low") + + # + # Request for an address in the proxy range but from an interface + # in a different VRF + # + self.send_and_assert_no_replies(self.pg3, arp_req_pg3, + "ARP req from different VRF") + + # + # Disable Each interface for proxy ARP + # - expect none to respond + # + for i in self.pg_interfaces: + i.set_proxy_arp(0) + + self.send_and_assert_no_replies(self.pg0, arp_req_pg0, + "ARP req from disable") + self.send_and_assert_no_replies(self.pg1, arp_req_pg1, + "ARP req from disable") + self.send_and_assert_no_replies(self.pg2, arp_req_pg2, + "ARP req from disable") + + # + # clean up on interface 2 + # + self.pg2.unset_unnumbered(self.pg1.sw_if_index) + + def test_mpls(self): + """ MPLS """ + + # + # Interface 2 does not yet have ip4 config + # + self.pg2.config_ip4() + self.pg2.generate_remote_hosts(2) + + # + # Add a reoute with out going label via an ARP unresolved next-hop + # + ip_10_0_0_1 = VppIpRoute(self, "10.0.0.1", 32, + [VppRoutePath(self.pg2.remote_hosts[1].ip4, + self.pg2.sw_if_index, + labels=[55])]) + ip_10_0_0_1.add_vpp_config() + + # + # packets should generate an ARP request + # + p = (Ether(src=self.pg0.remote_mac, + dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst="10.0.0.1") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100)) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_arp_req(rx[0], + self.pg2.local_mac, + self.pg2.local_ip4, + self.pg2._remote_hosts[1].ip4) + + # + # now resolve the neighbours + # + self.pg2.configure_ipv4_neighbors() + + # + # Now packet should be properly MPLS encapped. + # This verifies that MPLS link-type adjacencies are completed + # when the ARP entry resolves + # + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx = self.pg2.get_capture(1) + self.verify_ip_o_mpls(rx[0], + self.pg2.local_mac, + self.pg2.remote_hosts[1].mac, + 55, + self.pg0.remote_ip4, + "10.0.0.1") + self.pg2.unconfig_ip4() + + def test_arp_vrrp(self): + """ ARP reply with VRRP virtual src hw addr """ + + # + # IP packet destined for pg1 remote host arrives on pg0 resulting + # in an ARP request for the address of the remote host on pg1 + # + p0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg1.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_arp_req(rx1[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1.remote_ip4) + + # + # ARP reply for address of pg1 remote host arrives on pg1 with + # the hw src addr set to a value in the VRRP IPv4 range of + # MAC addresses + # + p1 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) / + ARP(op="is-at", hwdst=self.pg1.local_mac, + hwsrc="00:00:5e:00:01:09", pdst=self.pg1.local_ip4, + psrc=self.pg1.remote_ip4)) + + self.pg1.add_stream(p1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # + # IP packet destined for pg1 remote host arrives on pg0 again. + # VPP should have an ARP entry for that address now and the packet + # should be sent out pg1. + # + self.pg0.add_stream(p0) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_ip(rx1[0], + self.pg1.local_mac, + "00:00:5e:00:01:09", + self.pg0.remote_ip4, + self.pg1.remote_ip4) + + self.pg1.admin_down() + self.pg1.admin_up() + + def test_arp_duplicates(self): + """ ARP Duplicates""" + + # + # Generate some hosts on the LAN + # + self.pg1.generate_remote_hosts(3) + + # + # Add host 1 on pg1 and pg2 + # + arp_pg1 = VppNeighbor(self, + self.pg1.sw_if_index, + self.pg1.remote_hosts[1].mac, + self.pg1.remote_hosts[1].ip4) + arp_pg1.add_vpp_config() + arp_pg2 = VppNeighbor(self, + self.pg2.sw_if_index, + self.pg2.remote_mac, + self.pg1.remote_hosts[1].ip4) + arp_pg2.add_vpp_config() + + # + # IP packet destined for pg1 remote host arrives on pg1 again. + # + p = (Ether(dst=self.pg0.local_mac, + src=self.pg0.remote_mac) / + IP(src=self.pg0.remote_ip4, + dst=self.pg1.remote_hosts[1].ip4) / + UDP(sport=1234, dport=1234) / + Raw()) + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_ip(rx1[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip4, + self.pg1.remote_hosts[1].ip4) + + # + # remove the duplicate on pg1 + # packet stream shoud generate ARPs out of pg1 + # + arp_pg1.remove_vpp_config() + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_arp_req(rx1[0], + self.pg1.local_mac, + self.pg1.local_ip4, + self.pg1.remote_hosts[1].ip4) + + # + # Add it back + # + arp_pg1.add_vpp_config() + + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx1 = self.pg1.get_capture(1) + + self.verify_ip(rx1[0], + self.pg1.local_mac, + self.pg1.remote_hosts[1].mac, + self.pg0.remote_ip4, + self.pg1.remote_hosts[1].ip4) + + def test_arp_static(self): + """ ARP Static""" + self.pg2.generate_remote_hosts(3) + + # + # Add a static ARP entry + # + static_arp = VppNeighbor(self, + self.pg2.sw_if_index, + self.pg2.remote_hosts[1].mac, + self.pg2.remote_hosts[1].ip4, + is_static=1) + static_arp.add_vpp_config() + + # + # Add the connected prefix to the interface + # + self.pg2.config_ip4() + + # + # We should now find the adj-fib + # + self.assertTrue(find_nbr(self, + self.pg2.sw_if_index, + self.pg2.remote_hosts[1].ip4, + is_static=1)) + self.assertTrue(find_route(self, + self.pg2.remote_hosts[1].ip4, + 32)) + + # + # remove the connected + # + self.pg2.unconfig_ip4() + + # + # put the interface into table 1 + # + self.pg2.set_table_ip4(1) + + # + # configure the same connected and expect to find the + # adj fib in the new table + # + self.pg2.config_ip4() + self.assertTrue(find_route(self, + self.pg2.remote_hosts[1].ip4, + 32, + table_id=1)) + + # + # clean-up + # + self.pg2.unconfig_ip4() + self.pg2.set_table_ip4(0) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_p2p_ethernet.py b/test/test_p2p_ethernet.py new file mode 100644 index 00000000..8688f7e6 --- /dev/null +++ b/test/test_p2p_ethernet.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python +import random +import unittest +import datetime +import re + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 + +from framework import VppTestCase, VppTestRunner, running_extended_tests +from vpp_sub_interface import VppP2PSubint +from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto +from util import mactobinary + + +class P2PEthernetAPI(VppTestCase): + """P2P Ethernet tests""" + + p2p_sub_ifs = [] + + @classmethod + def setUpClass(cls): + super(P2PEthernetAPI, cls).setUpClass() + + # Create pg interfaces + cls.create_pg_interfaces(range(4)) + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + def create_p2p_ethernet(self, parent_if, sub_id, remote_mac): + p2p = VppP2PSubint(self, parent_if, sub_id, mactobinary(remote_mac)) + self.p2p_sub_ifs.append(p2p) + + def delete_p2p_ethernet(self, parent_if, remote_mac): + self.vapi.delete_p2pethernet_subif(parent_if.sw_if_index, + mactobinary(remote_mac)) + + def test_api(self): + """delete/create p2p subif""" + self.logger.info("FFP_TEST_START_0000") + + self.create_p2p_ethernet(self.pg0, 1, "de:ad:00:00:00:01") + self.create_p2p_ethernet(self.pg0, 2, "de:ad:00:00:00:02") + intfs = self.vapi.cli("show interface") + + self.assertNotEqual(intfs.find('pg0.1'), -1) + self.assertNotEqual(intfs.find('pg0.2'), -1) + self.assertEqual(intfs.find('pg0.5'), -1) + + # create pg2.5 subif + self.create_p2p_ethernet(self.pg0, 5, "de:ad:00:00:00:ff") + intfs = self.vapi.cli("show interface") + self.assertNotEqual(intfs.find('pg0.5'), -1) + # delete pg2.5 subif + self.delete_p2p_ethernet(self.pg0, "de:ad:00:00:00:ff") + + intfs = self.vapi.cli("show interface") + + self.assertNotEqual(intfs.find('pg0.1'), -1) + self.assertNotEqual(intfs.find('pg0.2'), -1) + self.assertEqual(intfs.find('pg0.5'), -1) + + self.logger.info("FFP_TEST_FINISH_0000") + + def test_p2p_subif_creation_1k(self): + """create 1k of p2p subifs""" + self.logger.info("FFP_TEST_START_0001") + + macs = [] + clients = 1000 + mac = int("dead00000000", 16) + + for i in range(1, clients+1): + try: + macs.append(':'.join(re.findall('..', '{:02x}'.format(mac+i)))) + self.vapi.create_p2pethernet_subif(self.pg2.sw_if_index, + mactobinary(macs[i-1]), + i) + except Exception: + print "Failed to create subif %d %s" % (i, macs[i-1]) + raise + + intfs = self.vapi.cli("show interface").split("\n") + count = 0 + for intf in intfs: + if intf.startswith('pg2.'): + count += 1 + self.assertEqual(count, clients) + + self.logger.info("FFP_TEST_FINISH_0001") + + @unittest.skipUnless(running_extended_tests(), "part of extended tests") + def test_p2p_subif_creation_10k(self): + """create 100k of p2p subifs""" + self.logger.info("FFP_TEST_START_0001") + + macs = [] + clients = 100000 + mac = int("dead00000000", 16) + + s_time = datetime.datetime.now() + for i in range(1, clients+1): + if i % 1000 == 0: + e_time = datetime.datetime.now() + print "Created 1000 subifs in %s secs" % (e_time - s_time) + s_time = e_time + try: + macs.append(':'.join(re.findall('..', '{:02x}'.format(mac+i)))) + self.vapi.create_p2pethernet_subif(self.pg3.sw_if_index, + mactobinary(macs[i-1]), + i) + except Exception: + print "Failed to create subif %d %s" % (i, macs[i-1]) + raise + + intfs = self.vapi.cli("show interface").split("\n") + count = 0 + for intf in intfs: + if intf.startswith('pg3.'): + count += 1 + self.assertEqual(count, clients) + + self.logger.info("FFP_TEST_FINISH_0001") + + +class P2PEthernetIPV6(VppTestCase): + """P2P Ethernet IPv6 tests""" + + p2p_sub_ifs = [] + packets = [] + + @classmethod + def setUpClass(cls): + super(P2PEthernetIPV6, cls).setUpClass() + + # Create pg interfaces + cls.create_pg_interfaces(range(3)) + + # Packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + cls.pg0.generate_remote_hosts(3) + cls.pg0.configure_ipv6_neighbors() + + cls.pg1.config_ip6() + cls.pg1.generate_remote_hosts(3) + cls.pg1.configure_ipv6_neighbors() + cls.pg1.disable_ipv6_ra() + + def setUp(self): + super(P2PEthernetIPV6, self).setUp() + for p in self.packets: + self.packets.remove(p) + self.create_p2p_ethernet(self.pg0, 1, self.pg0._remote_hosts[0].mac) + self.create_p2p_ethernet(self.pg0, 2, self.pg0._remote_hosts[1].mac) + self.p2p_sub_ifs[0].config_ip6() + self.p2p_sub_ifs[1].config_ip6() + self.vapi.cli("trace add p2p-ethernet-input 50") + + def tearDown(self): + self.delete_p2p_ethernet(self.pg0, self.pg0._remote_hosts[0].mac) + self.delete_p2p_ethernet(self.pg0, self.pg0._remote_hosts[1].mac) + super(P2PEthernetIPV6, self).tearDown() + + def create_p2p_ethernet(self, parent_if, sub_id, remote_mac): + p2p = VppP2PSubint(self, parent_if, sub_id, mactobinary(remote_mac)) + p2p.admin_up() + p2p.config_ip6() + p2p.disable_ipv6_ra() + self.p2p_sub_ifs.append(p2p) + + def delete_p2p_ethernet(self, parent_if, remote_mac): + self.vapi.delete_p2pethernet_subif(parent_if.sw_if_index, + mactobinary(remote_mac)) + + def create_stream(self, src_mac=None, dst_mac=None, + src_ip=None, dst_ip=None, size=None): + pkt_size = size + if size is None: + pkt_size = random.choice(self.pg_if_packet_sizes) + p = Ether(src=src_mac, dst=dst_mac) + p /= IPv6(src=src_ip, dst=dst_ip) + p /= (UDP(sport=1234, dport=4321) / Raw('\xa5' * 20)) + self.extend_packet(p, pkt_size) + return p + + def send_packets(self, src_if=None, dst_if=None, packets=None, count=None): + self.pg_enable_capture([dst_if]) + if packets is None: + packets = self.packets + src_if.add_stream(packets) + self.pg_start() + if count is None: + count = len(packets) + return dst_if.get_capture(count) + + def verify_counters(self, counter_id, expected_value): + counters = self.vapi.cli("sh errors").split('\n') + counter_value = -1 + for i in range(1, len(counters)-1): + results = counters[i].split() + if results[1] == counter_id: + counter_value = int(results[0]) + break + self.assertEqual(counter_value, expected_value) + + def test_no_p2p_subif(self): + """standard routing without p2p subinterfaces""" + self.logger.info("FFP_TEST_START_0001") + + route_8000 = VppIpRoute(self, "8000::", 64, + [VppRoutePath(self.pg0.remote_ip6, + self.pg0.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_8000.add_vpp_config() + + self.packets = [(Ether(dst=self.pg1.local_mac, + src=self.pg1.remote_mac) / + IPv6(src="3001::1", dst="8000::100") / + UDP(sport=1234, dport=1234) / + Raw('\xa5' * 100))] + self.send_packets(self.pg1, self.pg0) + + self.logger.info("FFP_TEST_FINISH_0001") + + def test_ip6_rx_p2p_subif(self): + """receive ipv6 packet via p2p subinterface""" + self.logger.info("FFP_TEST_START_0002") + + route_9001 = VppIpRoute(self, "9001::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_9001.add_vpp_config() + + self.packets.append( + self.create_stream(src_mac=self.pg0._remote_hosts[0].mac, + dst_mac=self.pg0.local_mac, + src_ip=self.p2p_sub_ifs[0].remote_ip6, + dst_ip="9001::100")) + + self.send_packets(self.pg0, self.pg1, self.packets) + self.verify_counters('p2p-ethernet-input', 1) + + route_9001.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_ip6_rx_p2p_subif_route(self): + """route rx ip6 packet not matching p2p subinterface""" + self.logger.info("FFP_TEST_START_0003") + + self.pg0.config_ip6() + + route_3 = VppIpRoute(self, "9000::", 64, + [VppRoutePath(self.pg1._remote_hosts[0].ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_3.add_vpp_config() + + self.packets.append( + self.create_stream(src_mac="02:03:00:00:ff:ff", + dst_mac=self.pg0.local_mac, + src_ip="a000::100", + dst_ip="9000::100")) + + self.send_packets(self.pg0, self.pg1) + + self.pg0.unconfig_ip6() + + route_3.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0003") + + def test_ip6_rx_p2p_subif_drop(self): + """drop rx packet not matching p2p subinterface""" + self.logger.info("FFP_TEST_START_0004") + + route_9001 = VppIpRoute(self, "9000::", 64, + [VppRoutePath(self.pg1._remote_hosts[0].ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_9001.add_vpp_config() + + self.packets.append( + self.create_stream(src_mac="02:03:00:00:ff:ff", + dst_mac=self.pg0.local_mac, + src_ip="a000::100", + dst_ip="9000::100")) + + # no packet received + self.send_packets(self.pg0, self.pg1, count=0) + self.logger.info("FFP_TEST_FINISH_0004") + + def test_ip6_tx_p2p_subif(self): + """send packet via p2p subinterface""" + self.logger.info("FFP_TEST_START_0005") + + route_8000 = VppIpRoute(self, "8000::", 64, + [VppRoutePath(self.pg0.remote_ip6, + self.pg0.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_8000.add_vpp_config() + route_8001 = VppIpRoute(self, "8001::", 64, + [VppRoutePath(self.p2p_sub_ifs[0].remote_ip6, + self.p2p_sub_ifs[0].sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_8001.add_vpp_config() + route_8002 = VppIpRoute(self, "8002::", 64, + [VppRoutePath(self.p2p_sub_ifs[1].remote_ip6, + self.p2p_sub_ifs[1].sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route_8002.add_vpp_config() + + for i in range(0, 3): + self.packets.append( + self.create_stream(src_mac=self.pg1.remote_mac, + dst_mac=self.pg1.local_mac, + src_ip=self.pg1.remote_ip6, + dst_ip="800%d::100" % i)) + + self.send_packets(self.pg1, self.pg0, count=3) + + route_8000.remove_vpp_config() + route_8001.remove_vpp_config() + route_8002.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0005") + + def test_ip6_tx_p2p_subif_drop(self): + """drop tx ip6 packet not matching p2p subinterface""" + self.logger.info("FFP_TEST_START_0006") + + self.packets.append( + self.create_stream(src_mac="02:03:00:00:ff:ff", + dst_mac=self.pg0.local_mac, + src_ip="a000::100", + dst_ip="9000::100")) + + # no packet received + self.send_packets(self.pg0, self.pg1, count=0) + self.logger.info("FFP_TEST_FINISH_0006") + + +class P2PEthernetIPV4(VppTestCase): + """P2P Ethernet IPv4 tests""" + + p2p_sub_ifs = [] + packets = [] + + @classmethod + def setUpClass(cls): + super(P2PEthernetIPV4, cls).setUpClass() + + # Create pg interfaces + cls.create_pg_interfaces(range(3)) + + # Packet sizes + cls.pg_if_packet_sizes = [64, 512, 1518, 9018] + + # Set up all interfaces + for i in cls.pg_interfaces: + i.admin_up() + + cls.pg0.config_ip4() + cls.pg0.generate_remote_hosts(5) + cls.pg0.configure_ipv4_neighbors() + + cls.pg1.config_ip4() + cls.pg1.generate_remote_hosts(5) + cls.pg1.configure_ipv4_neighbors() + + def setUp(self): + super(P2PEthernetIPV4, self).setUp() + for p in self.packets: + self.packets.remove(p) + self.create_p2p_ethernet(self.pg0, 1, self.pg0._remote_hosts[0].mac) + self.create_p2p_ethernet(self.pg0, 2, self.pg0._remote_hosts[1].mac) + self.p2p_sub_ifs[0].config_ip4() + self.p2p_sub_ifs[1].config_ip4() + self.vapi.cli("trace add p2p-ethernet-input 50") + + def tearDown(self): + self.delete_p2p_ethernet(self.pg0, self.pg0._remote_hosts[0].mac) + self.delete_p2p_ethernet(self.pg0, self.pg0._remote_hosts[1].mac) + super(P2PEthernetIPV4, self).tearDown() + + def create_stream(self, src_mac=None, dst_mac=None, + src_ip=None, dst_ip=None, size=None): + pkt_size = size + if size is None: + pkt_size = random.choice(self.pg_if_packet_sizes) + p = Ether(src=src_mac, dst=dst_mac) + p /= IP(src=src_ip, dst=dst_ip) + p /= (UDP(sport=1234, dport=4321) / Raw('\xa5' * 20)) + self.extend_packet(p, pkt_size) + return p + + def send_packets(self, src_if=None, dst_if=None, packets=None, count=None): + self.pg_enable_capture([dst_if]) + if packets is None: + packets = self.packets + src_if.add_stream(packets) + self.pg_start() + if count is None: + count = len(packets) + return dst_if.get_capture(count) + + def verify_counters(self, counter_id, expected_value): + counters = self.vapi.cli("sh errors").split('\n') + counter_value = -1 + for i in range(1, len(counters)-1): + results = counters[i].split() + if results[1] == counter_id: + counter_value = int(results[0]) + break + self.assertEqual(counter_value, expected_value) + + def create_p2p_ethernet(self, parent_if, sub_id, remote_mac): + p2p = VppP2PSubint(self, parent_if, sub_id, mactobinary(remote_mac)) + p2p.admin_up() + p2p.config_ip4() + self.p2p_sub_ifs.append(p2p) + + def delete_p2p_ethernet(self, parent_if, remote_mac): + self.vapi.delete_p2pethernet_subif(parent_if.sw_if_index, + mactobinary(remote_mac)) + + def test_ip4_rx_p2p_subif(self): + """receive ipv4 packet via p2p subinterface""" + self.logger.info("FFP_TEST_START_0002") + + route_9000 = VppIpRoute(self, "9.0.0.0", 16, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_9000.add_vpp_config() + + self.packets.append( + self.create_stream(src_mac=self.pg0._remote_hosts[0].mac, + dst_mac=self.pg0.local_mac, + src_ip=self.p2p_sub_ifs[0].remote_ip4, + dst_ip="9.0.0.100")) + + self.send_packets(self.pg0, self.pg1, self.packets) + + self.verify_counters('p2p-ethernet-input', 1) + + route_9000.remove_vpp_config() + self.logger.info("FFP_TEST_FINISH_0002") + + def test_ip4_rx_p2p_subif_route(self): + """route rx packet not matching p2p subinterface""" + self.logger.info("FFP_TEST_START_0003") + + route_9001 = VppIpRoute(self, "9.0.0.0", 24, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_9001.add_vpp_config() + + self.packets.append( + self.create_stream(src_mac="02:01:00:00:ff:ff", + dst_mac=self.pg0.local_mac, + src_ip="8.0.0.100", + dst_ip="9.0.0.100")) + + self.send_packets(self.pg0, self.pg1) + + route_9001.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0003") + + def test_ip4_tx_p2p_subif(self): + """send ip4 packet via p2p subinterface""" + self.logger.info("FFP_TEST_START_0005") + + route_9100 = VppIpRoute(self, "9.1.0.100", 24, + [VppRoutePath(self.pg0.remote_ip4, + self.pg0.sw_if_index, + )]) + route_9100.add_vpp_config() + route_9200 = VppIpRoute(self, "9.2.0.100", 24, + [VppRoutePath(self.p2p_sub_ifs[0].remote_ip4, + self.p2p_sub_ifs[0].sw_if_index, + )]) + route_9200.add_vpp_config() + route_9300 = VppIpRoute(self, "9.3.0.100", 24, + [VppRoutePath(self.p2p_sub_ifs[1].remote_ip4, + self.p2p_sub_ifs[1].sw_if_index + )]) + route_9300.add_vpp_config() + + for i in range(0, 3): + self.packets.append( + self.create_stream(src_mac=self.pg1.remote_mac, + dst_mac=self.pg1.local_mac, + src_ip=self.pg1.remote_ip4, + dst_ip="9.%d.0.100" % (i+1))) + + self.send_packets(self.pg1, self.pg0) + + # route_7000.remove_vpp_config() + route_9100.remove_vpp_config() + route_9200.remove_vpp_config() + route_9300.remove_vpp_config() + + self.logger.info("FFP_TEST_FINISH_0005") + + def test_ip4_tx_p2p_subif_drop(self): + """drop tx ip4 packet not matching p2p subinterface""" + self.logger.info("FFP_TEST_START_0006") + + self.packets.append( + self.create_stream(src_mac="02:01:00:00:ff:ff", + dst_mac=self.pg0.local_mac, + src_ip="8.0.0.100", + dst_ip="9.0.0.100")) + + # no packet received + self.send_packets(self.pg0, self.pg1, count=0) + self.logger.info("FFP_TEST_FINISH_0006") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_papi.py b/test/test_papi.py new file mode 100644 index 00000000..1a5f6ae6 --- /dev/null +++ b/test/test_papi.py @@ -0,0 +1,31 @@ +import binascii +from framework import VppTestCase + +""" TestPAPI is a subclass of VPPTestCase classes. + +Basic test for sanity check of the Python API binding. + +""" + + +class TestPAPI(VppTestCase): + """ PAPI Test Case """ + + @classmethod + def setUpClass(cls): + super(TestPAPI, cls).setUpClass() + cls.v = cls.vapi.papi + + def test_show_version(self): + rv = self.v.show_version() + self.assertEqual(rv.retval, 0) + + def test_show_version_invalid_param(self): + self.assertRaises(ValueError, self.v.show_version, foobar='foo') + + def test_u8_array(self): + rv = self.v.get_node_index(node_name='ip4-lookup') + self.assertEqual(rv.retval, 0) + node_name = 'X' * 100 + self.assertRaises(ValueError, self.v.get_node_index, + node_name=node_name) diff --git a/test/test_ping.py b/test/test_ping.py new file mode 100644 index 00000000..4f3921e9 --- /dev/null +++ b/test/test_ping.py @@ -0,0 +1,118 @@ +import socket + +from scapy.layers.inet import IP, UDP, ICMP +from scapy.layers.inet6 import IPv6 +from scapy.layers.l2 import Ether, GRE +from scapy.packet import Raw + +from framework import VppTestCase +from util import ppp + +""" TestPing is a subclass of VPPTestCase classes. + +Basic test for sanity check of the ping. + +""" + + +class TestPing(VppTestCase): + """ Ping Test Case """ + + @classmethod + def setUpClass(cls): + super(TestPing, cls).setUpClass() + try: + cls.create_pg_interfaces(range(2)) + cls.interfaces = list(cls.pg_interfaces) + + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.config_ip6() + i.disable_ipv6_ra() + i.resolve_arp() + i.resolve_ndp() + except Exception: + super(TestPing, cls).tearDownClass() + raise + + def tearDown(self): + super(TestPing, self).tearDown() + if not self.vpp_dead: + self.vapi.cli("show hardware") + + def test_ping_basic(self): + """ basic ping test """ + try: + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.logger.info(self.vapi.cli("show ip arp")) + self.logger.info(self.vapi.cli("show ip6 neighbors")) + + remote_ip4 = self.pg1.remote_ip4 + ping_cmd = "ping " + remote_ip4 + " interval 0.01 repeat 10" + ret = self.vapi.cli(ping_cmd) + self.logger.info(ret) + out = self.pg1.get_capture(10) + icmp_id = None + icmp_seq = 1 + for p in out: + ip = p[IP] + self.assertEqual(ip.version, 4) + self.assertEqual(ip.flags, 0) + self.assertEqual(ip.src, self.pg1.local_ip4) + self.assertEqual(ip.dst, self.pg1.remote_ip4) + self.assertEqual(ip.proto, 1) + self.assertEqual(len(ip.options), 0) + self.assertGreaterEqual(ip.ttl, 254) + icmp = p[ICMP] + self.assertEqual(icmp.type, 8) + self.assertEqual(icmp.code, 0) + self.assertEqual(icmp.seq, icmp_seq) + icmp_seq = icmp_seq + 1 + if icmp_id is None: + icmp_id = icmp.id + else: + self.assertEqual(icmp.id, icmp_id) + finally: + self.vapi.cli("show error") + + def test_ping_burst(self): + """ burst ping test """ + try: + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + self.logger.info(self.vapi.cli("show ip arp")) + self.logger.info(self.vapi.cli("show ip6 neighbors")) + + remote_ip4 = self.pg1.remote_ip4 + ping_cmd = "ping " + remote_ip4 + " interval 0.01 burst 3" + ret = self.vapi.cli(ping_cmd) + self.logger.info(ret) + out = self.pg1.get_capture(3*5) + icmp_id = None + icmp_seq = 1 + count = 0 + for p in out: + ip = p[IP] + self.assertEqual(ip.version, 4) + self.assertEqual(ip.flags, 0) + self.assertEqual(ip.src, self.pg1.local_ip4) + self.assertEqual(ip.dst, self.pg1.remote_ip4) + self.assertEqual(ip.proto, 1) + self.assertEqual(len(ip.options), 0) + self.assertGreaterEqual(ip.ttl, 254) + icmp = p[ICMP] + self.assertEqual(icmp.type, 8) + self.assertEqual(icmp.code, 0) + self.assertEqual(icmp.seq, icmp_seq) + count = count + 1 + if count >= 3: + icmp_seq = icmp_seq + 1 + count = 0 + if icmp_id is None: + icmp_id = icmp.id + else: + self.assertEqual(icmp.id, icmp_id) + finally: + self.vapi.cli("show error") diff --git a/test/test_pppoe.py b/test/test_pppoe.py new file mode 100644 index 00000000..1d0aeffd --- /dev/null +++ b/test/test_pppoe.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python + +import unittest +from logging import * + +from framework import VppTestCase, VppTestRunner +from vpp_ip_route import VppIpRoute, VppRoutePath +from vpp_pppoe_interface import VppPppoeInterface, VppPppoe6Interface +from vpp_papi_provider import L2_VTR_OP + +from scapy.packet import Raw +from scapy.layers.l2 import Ether +from scapy.layers.ppp import PPPoE, PPPoED, PPP +from scapy.layers.inet import IP, UDP +from scapy.layers.inet6 import IPv6 +from scapy.volatile import RandMAC, RandIP + +from util import ppp, ppc, mactobinary +import socket + + +class TestPPPoE(VppTestCase): + """ PPPoE Test Case """ + + @classmethod + def setUpClass(cls): + super(TestPPPoE, cls).setUpClass() + + cls.session_id = 1 + cls.dst_ip = "100.1.1.100" + cls.dst_ipn = socket.inet_pton(socket.AF_INET, cls.dst_ip) + + def setUp(self): + super(TestPPPoE, self).setUp() + + # create 2 pg interfaces + self.create_pg_interfaces(range(3)) + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + def tearDown(self): + super(TestPPPoE, self).tearDown() + + self.logger.info(self.vapi.cli("show int")) + self.logger.info(self.vapi.cli("show pppoe fib")) + self.logger.info(self.vapi.cli("show pppoe session")) + self.logger.info(self.vapi.cli("show ip fib")) + self.logger.info(self.vapi.cli("show trace")) + + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + + def create_stream_pppoe_discovery(self, src_if, dst_if, + client_mac, count=1): + packets = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + p = (Ether(dst=src_if.local_mac, src=client_mac) / + PPPoED(sessionid=0) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + packets.append(p) + + # return the created packet list + return packets + + def create_stream_pppoe_lcp(self, src_if, dst_if, + client_mac, session_id, count=1): + packets = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + p = (Ether(dst=src_if.local_mac, src=client_mac) / + PPPoE(sessionid=session_id) / + PPP(proto=0xc021) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + packets.append(p) + + # return the created packet list + return packets + + def create_stream_pppoe_ip4(self, src_if, dst_if, + client_mac, session_id, client_ip, count=1): + packets = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + p = (Ether(dst=src_if.local_mac, src=client_mac) / + PPPoE(sessionid=session_id) / + PPP(proto=0x0021) / + IP(src=client_ip, dst=self.dst_ip) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + packets.append(p) + + # return the created packet list + return packets + + def create_stream_ip4(self, src_if, dst_if, client_ip, dst_ip, count=1): + pkts = [] + for i in range(count): + # create packet info stored in the test case instance + info = self.create_packet_info(src_if, dst_if) + # convert the info into packet payload + payload = self.info_to_payload(info) + # create the packet itself + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=dst_ip, dst=client_ip) / + Raw(payload)) + # store a copy of the packet in the packet info + info.data = p.copy() + # append the packet to the list + pkts.append(p) + + # return the created packet list + return pkts + + def verify_decapped_pppoe(self, src_if, capture, sent): + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def verify_encaped_pppoe(self, src_if, capture, sent, session_id): + + self.assertEqual(len(capture), len(sent)) + + for i in range(len(capture)): + try: + tx = sent[i] + rx = capture[i] + + tx_ip = tx[IP] + rx_ip = rx[IP] + + self.assertEqual(rx_ip.src, tx_ip.src) + self.assertEqual(rx_ip.dst, tx_ip.dst) + + rx_pppoe = rx[PPPoE] + + self.assertEqual(rx_pppoe.sessionid, session_id) + + except: + self.logger.error(ppp("Rx:", rx)) + self.logger.error(ppp("Tx:", tx)) + raise + + def test_PPPoE_Decap(self): + """ PPPoE Decap Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session + pppoe_if = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if.add_vpp_config() + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded + # + tx2 = self.create_stream_pppoe_ip4(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id, + self.pg0.remote_ip4) + self.pg0.add_stream(tx2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx2 = self.pg1.get_capture(len(tx2)) + self.verify_decapped_pppoe(self.pg0, rx2, tx2) + + self.logger.info(self.vapi.cli("show pppoe fib")) + self.logger.info(self.vapi.cli("show pppoe session")) + self.logger.info(self.vapi.cli("show ip fib")) + + # + # test case cleanup + # + + # Delete PPPoE session + pppoe_if.remove_vpp_config() + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + + def test_PPPoE_Encap(self): + """ PPPoE Encap Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session + pppoe_if = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if.add_vpp_config() + + # + # Send a packet stream that is routed into the session + # - packets are PPPoE encapped + # + self.vapi.cli("clear trace") + tx2 = self.create_stream_ip4(self.pg1, self.pg0, + self.pg0.remote_ip4, self.dst_ip, 65) + self.pg1.add_stream(tx2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx2 = self.pg0.get_capture(len(tx2)) + self.verify_encaped_pppoe(self.pg1, rx2, tx2, self.session_id) + + self.logger.info(self.vapi.cli("show pppoe fib")) + self.logger.info(self.vapi.cli("show pppoe session")) + self.logger.info(self.vapi.cli("show ip fib")) + self.logger.info(self.vapi.cli("show adj")) + + # + # test case cleanup + # + + # Delete PPPoE session + pppoe_if.remove_vpp_config() + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + + def test_PPPoE_Add_Twice(self): + """ PPPoE Add Same Session Twice Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session + pppoe_if = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if.add_vpp_config() + + # + # The double create (create the same session twice) should fail, + # and we should still be able to use the original + # + try: + gre_if.add_vpp_config() + except Exception: + pass + else: + self.fail("Double GRE tunnel add does not fail") + + # + # test case cleanup + # + + # Delete PPPoE session + pppoe_if.remove_vpp_config() + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + + def test_PPPoE_Del_Twice(self): + """ PPPoE Delete Same Session Twice Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session + pppoe_if = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if.add_vpp_config() + + # Delete PPPoE session + pppoe_if.remove_vpp_config() + + # + # The double del (del the same session twice) should fail, + # and we should still be able to use the original + # + try: + gre_if.remove_vpp_config() + except Exception: + pass + else: + self.fail("Double GRE tunnel del does not fail") + + # + # test case cleanup + # + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + + def test_PPPoE_Decap_Multiple(self): + """ PPPoE Decap Multiple Sessions Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery 1 + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP 1 + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session 1 + pppoe_if1 = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if1.add_vpp_config() + + # Send PPPoE Discovery 2 + tx3 = self.create_stream_pppoe_discovery(self.pg2, self.pg1, + self.pg2.remote_mac) + self.pg2.add_stream(tx3) + self.pg_start() + + # Send PPPoE PPP LCP 2 + tx4 = self.create_stream_pppoe_lcp(self.pg2, self.pg1, + self.pg2.remote_mac, + self.session_id + 1) + self.pg2.add_stream(tx4) + self.pg_start() + + # Create PPPoE session 2 + pppoe_if2 = VppPppoeInterface(self, + self.pg2.remote_ip4, + self.pg2.remote_mac, + self.session_id + 1) + pppoe_if2.add_vpp_config() + + # + # Send tunneled packets that match the created tunnel and + # are decapped and forwarded + # + tx2 = self.create_stream_pppoe_ip4(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id, + self.pg0.remote_ip4) + self.pg0.add_stream(tx2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx2 = self.pg1.get_capture(len(tx2)) + self.verify_decapped_pppoe(self.pg0, rx2, tx2) + + tx5 = self.create_stream_pppoe_ip4(self.pg2, self.pg1, + self.pg2.remote_mac, + self.session_id + 1, + self.pg2.remote_ip4) + self.pg2.add_stream(tx5) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx5 = self.pg1.get_capture(len(tx5)) + self.verify_decapped_pppoe(self.pg2, rx5, tx5) + + self.logger.info(self.vapi.cli("show pppoe fib")) + self.logger.info(self.vapi.cli("show pppoe session")) + self.logger.info(self.vapi.cli("show ip fib")) + + # + # test case cleanup + # + + # Delete PPPoE session + pppoe_if1.remove_vpp_config() + pppoe_if2.remove_vpp_config() + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + + def test_PPPoE_Encap_Multiple(self): + """ PPPoE Encap Multiple Sessions Test """ + + self.vapi.cli("clear trace") + + # + # Add a route that resolves the server's destination + # + route_sever_dst = VppIpRoute(self, "100.1.1.100", 32, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index)]) + route_sever_dst.add_vpp_config() + + # Send PPPoE Discovery 1 + tx0 = self.create_stream_pppoe_discovery(self.pg0, self.pg1, + self.pg0.remote_mac) + self.pg0.add_stream(tx0) + self.pg_start() + + # Send PPPoE PPP LCP 1 + tx1 = self.create_stream_pppoe_lcp(self.pg0, self.pg1, + self.pg0.remote_mac, + self.session_id) + self.pg0.add_stream(tx1) + self.pg_start() + + # Create PPPoE session 1 + pppoe_if1 = VppPppoeInterface(self, + self.pg0.remote_ip4, + self.pg0.remote_mac, + self.session_id) + pppoe_if1.add_vpp_config() + + # Send PPPoE Discovery 2 + tx3 = self.create_stream_pppoe_discovery(self.pg2, self.pg1, + self.pg2.remote_mac) + self.pg2.add_stream(tx3) + self.pg_start() + + # Send PPPoE PPP LCP 2 + tx4 = self.create_stream_pppoe_lcp(self.pg2, self.pg1, + self.pg2.remote_mac, + self.session_id + 1) + self.pg2.add_stream(tx4) + self.pg_start() + + # Create PPPoE session 2 + pppoe_if2 = VppPppoeInterface(self, + self.pg2.remote_ip4, + self.pg2.remote_mac, + self.session_id + 1) + pppoe_if2.add_vpp_config() + + # + # Send a packet stream that is routed into the session + # - packets are PPPoE encapped + # + self.vapi.cli("clear trace") + tx2 = self.create_stream_ip4(self.pg1, self.pg0, + self.pg0.remote_ip4, self.dst_ip) + self.pg1.add_stream(tx2) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx2 = self.pg0.get_capture(len(tx2)) + self.verify_encaped_pppoe(self.pg1, rx2, tx2, self.session_id) + + tx5 = self.create_stream_ip4(self.pg1, self.pg2, + self.pg2.remote_ip4, self.dst_ip) + self.pg1.add_stream(tx5) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + rx5 = self.pg2.get_capture(len(tx5)) + self.verify_encaped_pppoe(self.pg1, rx5, tx5, self.session_id + 1) + + self.logger.info(self.vapi.cli("show pppoe fib")) + self.logger.info(self.vapi.cli("show pppoe session")) + self.logger.info(self.vapi.cli("show ip fib")) + + # + # test case cleanup + # + + # Delete PPPoE session + pppoe_if1.remove_vpp_config() + pppoe_if2.remove_vpp_config() + + # Delete a route that resolves the server's destination + route_sever_dst.remove_vpp_config() + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_span.py b/test/test_span.py new file mode 100644 index 00000000..f2529e8f --- /dev/null +++ b/test/test_span.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python + +import unittest + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q, GRE +from scapy.layers.inet import IP, UDP +from scapy.layers.vxlan import VXLAN + +from framework import VppTestCase, VppTestRunner +from util import Host, ppp +from vpp_sub_interface import VppDot1QSubint, VppDot1ADSubint +from vpp_gre_interface import VppGreInterface, VppGre6Interface +from vpp_papi_provider import L2_VTR_OP +from collections import namedtuple + +Tag = namedtuple('Tag', ['dot1', 'vlan']) +DOT1AD = 0x88A8 +DOT1Q = 0x8100 + + +class TestSpan(VppTestCase): + """ SPAN Test Case """ + + @classmethod + def setUpClass(cls): + super(TestSpan, cls).setUpClass() + # Test variables + cls.hosts_nr = 10 # Number of hosts + cls.pkts_per_burst = 257 # Number of packets per burst + # create 3 pg interfaces + cls.create_pg_interfaces(range(3)) + + cls.bd_id = 55 + cls.sub_if = VppDot1QSubint(cls, cls.pg0, 100) + cls.dst_sub_if = VppDot1QSubint(cls, cls.pg2, 300) + cls.dst_sub_if.set_vtr(L2_VTR_OP.L2_POP_1, tag=300) + # packet flows mapping pg0 -> pg1, pg2 -> pg3, etc. + cls.flows = dict() + cls.flows[cls.pg0] = [cls.pg1] + + # packet sizes + cls.pg_if_packet_sizes = [64, 512] # , 1518, 9018] + + cls.interfaces = list(cls.pg_interfaces) + + # Create host MAC and IPv4 lists + # cls.MY_MACS = dict() + # cls.MY_IP4S = dict() + cls.create_host_lists(cls.hosts_nr) + + # setup all interfaces + for i in cls.interfaces: + i.admin_up() + i.config_ip4() + i.resolve_arp() + + cls.vxlan = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg2.local_ip4n, + dst_addr=cls.pg2.remote_ip4n, + vni=1111, + is_add=1) + + def setUp(self): + super(TestSpan, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + super(TestSpan, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show interface span")) + + def xconnect(self, a, b, is_add=1): + self.vapi.sw_interface_set_l2_xconnect(a, b, enable=is_add) + self.vapi.sw_interface_set_l2_xconnect(b, a, enable=is_add) + + def bridge(self, sw_if_index, is_add=1): + self.vapi.sw_interface_set_l2_bridge( + sw_if_index, bd_id=self.bd_id, enable=is_add) + + def _remove_tag(self, packet, vlan, tag_type): + self.assertEqual(packet.type, tag_type) + payload = packet.payload + self.assertEqual(payload.vlan, vlan) + inner_type = payload.type + payload = payload.payload + packet.remove_payload() + packet.add_payload(payload) + packet.type = inner_type + + def remove_tags(self, packet, tags): + for t in tags: + self._remove_tag(packet, t.vlan, t.dot1) + return packet + + def decap_gre(self, pkt): + """ + Decapsulate the original payload frame by removing GRE header + """ + self.assertEqual(pkt[Ether].src, self.pg2.local_mac) + self.assertEqual(pkt[Ether].dst, self.pg2.remote_mac) + + self.assertEqual(pkt[IP].src, self.pg2.local_ip4) + self.assertEqual(pkt[IP].dst, self.pg2.remote_ip4) + + return pkt[GRE].payload + + def decap_vxlan(self, pkt): + """ + Decapsulate the original payload frame by removing VXLAN header + """ + self.assertEqual(pkt[Ether].src, self.pg2.local_mac) + self.assertEqual(pkt[Ether].dst, self.pg2.remote_mac) + + self.assertEqual(pkt[IP].src, self.pg2.local_ip4) + self.assertEqual(pkt[IP].dst, self.pg2.remote_ip4) + + return pkt[VXLAN].payload + + @classmethod + def create_host_lists(self, count): + """ Method to create required number of MAC and IPv4 addresses. + Create required number of host MAC addresses and distribute them among + interfaces. Create host IPv4 address for every host MAC address too. + + :param count: Number of hosts to create MAC and IPv4 addresses for. + """ + # mapping between packet-generator index and lists of test hosts + self.hosts_by_pg_idx = dict() + + for pg_if in self.pg_interfaces: + # self.MY_MACS[i.sw_if_index] = [] + # self.MY_IP4S[i.sw_if_index] = [] + self.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = self.hosts_by_pg_idx[pg_if.sw_if_index] + for j in range(0, count): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + hosts.append(host) + + def create_stream(self, src_if, packet_sizes, do_dot1=False): + pkts = [] + for i in range(0, self.pkts_per_burst): + dst_if = self.flows[src_if][0] + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + if do_dot1: + p = self.sub_if.add_dot1_layer(p) + pkt_info.data = p.copy() + size = packet_sizes[(i / 2) % len(packet_sizes)] + self.extend_packet(p, size) + pkts.append(p) + return pkts + + def verify_capture(self, dst_if, capture_pg1, capture_pg2): + last_info = dict() + for i in self.interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = dst_if.sw_if_index + self.assertEqual( + len(capture_pg1), + len(capture_pg2), + "Different number of outgoing and mirrored packets : %u != %u" % + (len(capture_pg1), + len(capture_pg2))) + for pkt_pg1, pkt_pg2 in zip(capture_pg1, capture_pg2): + try: + ip1 = pkt_pg1[IP] + udp1 = pkt_pg1[UDP] + raw1 = pkt_pg1[Raw] + + if pkt_pg1[Ether] != pkt_pg2[Ether]: + self.logger.error("Different ethernet header of " + "outgoing and mirrored packet") + raise + if ip1 != pkt_pg2[IP]: + self.logger.error( + "Different ip header of outgoing and mirrored packet") + raise + if udp1 != pkt_pg2[UDP]: + self.logger.error( + "Different udp header of outgoing and mirrored packet") + raise + if raw1 != pkt_pg2[Raw]: + self.logger.error( + "Different raw data of outgoing and mirrored packet") + raise + + payload_info = self.payload_to_info(str(raw1)) + packet_index = payload_info.index + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug( + "Got packet on port %s: src=%u (id=%u)" % + (dst_if.name, payload_info.src, packet_index)) + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + self.assertTrue(next_info is not None) + self.assertEqual(packet_index, next_info.index) + saved_packet = next_info.data + # Check standard fields + self.assertEqual(ip1.src, saved_packet[IP].src) + self.assertEqual(ip1.dst, saved_packet[IP].dst) + self.assertEqual(udp1.sport, saved_packet[UDP].sport) + self.assertEqual(udp1.dport, saved_packet[UDP].dport) + except: + self.logger.error("Unexpected or invalid packets:") + self.logger.error(ppp("pg1 packet:", pkt_pg1)) + self.logger.error(ppp("pg2 packet:", pkt_pg2)) + raise + for i in self.interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Port %u: Packet expected from source %u didn't" + " arrive" % (dst_sw_if_index, i.sw_if_index)) + + def test_device_span(self): + """ SPAN device rx mirror test + + Test scenario: + 1. config + 3 interfaces, pg0 l2xconnected with pg1 + 2. sending l2 eth packets between 2 interfaces (pg0, pg1) and + mirrored to pg2 + 64B, 512B, 1518B, 9018B (ether_size) + burst of packets per interface + """ + + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.pg0.sw_if_index, self.pg1.sw_if_index) + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes) + self.pg0.add_stream(pkts) + + # Enable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.pg0.sw_if_index, self.pg2.sw_if_index) + + self.logger.info(self.vapi.ppcli("show interface span")) + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + self.verify_capture( + self.pg1, + self.pg1.get_capture(), + self.pg2.get_capture(pg2_expected)) + + # Disable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.pg0.sw_if_index, self.pg2.sw_if_index, state=0) + self.xconnect(self.pg0.sw_if_index, self.pg1.sw_if_index, is_add=0) + + def test_span_l2_rx(self): + """ SPAN l2 rx mirror test """ + + self.sub_if.admin_up() + + self.bridge(self.pg2.sw_if_index) + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index) + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream( + self.pg0, self.pg_if_packet_sizes, do_dot1=True) + self.pg0.add_stream(pkts) + + # Enable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.pg2.sw_if_index, is_l2=1) + + self.logger.info(self.vapi.ppcli("show interface span")) + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + pg1_pkts = self.pg1.get_capture() + pg2_pkts = self.pg2.get_capture(pg2_expected) + self.verify_capture( + self.pg1, + pg1_pkts, + pg2_pkts) + + self.bridge(self.pg2.sw_if_index, is_add=0) + # Disable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.pg2.sw_if_index, state=0, is_l2=1) + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + + def test_span_l2_rx_dst_vxlan(self): + """ SPAN l2 rx mirror into vxlan test """ + + self.sub_if.admin_up() + self.vapi.sw_interface_set_flags(self.vxlan.sw_if_index, + admin_up_down=1) + + self.bridge(self.vxlan.sw_if_index, is_add=1) + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index) + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream( + self.pg0, self.pg_if_packet_sizes, do_dot1=True) + self.pg0.add_stream(pkts) + + # Enable SPAN on pg0 sub if (mirrored to vxlan) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.vxlan.sw_if_index, is_l2=1) + + self.logger.info(self.vapi.ppcli("show interface span")) + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + pg1_pkts = self.pg1.get_capture() + pg2_pkts = [self.decap_vxlan(p) + for p in self.pg2.get_capture(pg2_expected)] + self.verify_capture( + self.pg1, + pg1_pkts, + pg2_pkts) + + self.bridge(self.vxlan.sw_if_index, is_add=0) + # Disable SPAN on pg0 sub if (mirrored to vxlan) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.vxlan.sw_if_index, state=0, is_l2=1) + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + + def test_span_l2_rx_dst_gre_subif_vtr(self): + """ SPAN l2 rx mirror into gre-subif+vtr """ + + self.sub_if.admin_up() + + gre_if = VppGreInterface(self, self.pg2.local_ip4, + self.pg2.remote_ip4, + is_teb=1) + + gre_if.add_vpp_config() + gre_if.admin_up() + + gre_sub_if = VppDot1QSubint(self, gre_if, 500) + gre_sub_if.set_vtr(L2_VTR_OP.L2_POP_1, tag=500) + gre_sub_if.admin_up() + + self.bridge(gre_sub_if.sw_if_index) + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=1) + + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream( + self.pg0, self.pg_if_packet_sizes, do_dot1=True) + self.pg0.add_stream(pkts) + + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_sub_if.sw_if_index, is_l2=1) + + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + pg1_pkts = self.pg1.get_capture() + pg2_pkts = self.pg2.get_capture(pg2_expected) + pg2_decaped = [self.remove_tags(self.decap_gre( + p), [Tag(dot1=DOT1Q, vlan=500)]) for p in pg2_pkts] + self.verify_capture( + self.pg1, + pg1_pkts, + pg2_decaped) + + self.bridge(gre_sub_if.sw_if_index, is_add=0) + # Disable SPAN on pg0 sub if + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_sub_if.sw_if_index, state=0, + is_l2=1) + gre_if.remove_vpp_config() + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + + def test_span_l2_rx_dst_vtr(self): + """ SPAN l2 rx mirror into subif+vtr """ + + self.sub_if.admin_up() + self.dst_sub_if.admin_up() + + self.bridge(self.dst_sub_if.sw_if_index) + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=1) + + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream( + self.pg0, self.pg_if_packet_sizes, do_dot1=True) + self.pg0.add_stream(pkts) + + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.dst_sub_if.sw_if_index, is_l2=1) + + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + pg1_pkts = self.pg1.get_capture() + pg2_pkts = self.pg2.get_capture(pg2_expected) + pg2_untagged = [self.remove_tags(p, [Tag(dot1=DOT1Q, vlan=300)]) + for p in pg2_pkts] + self.verify_capture( + self.pg1, + pg1_pkts, + pg2_untagged) + + self.bridge(self.dst_sub_if.sw_if_index, is_add=0) + # Disable SPAN on pg0 sub if (mirrored to vxlan) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, self.dst_sub_if.sw_if_index, state=0, + is_l2=1) + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + + def test_l2_tx_span(self): + """ SPAN l2 tx mirror test """ + + self.sub_if.admin_up() + self.bridge(self.pg2.sw_if_index) + # Create bi-directional cross-connects between pg0 and pg1 + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index) + # Create incoming packet streams for packet-generator interfaces + pkts = self.create_stream( + self.pg0, self.pg_if_packet_sizes, do_dot1=True) + self.pg0.add_stream(pkts) + + # Enable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.pg1.sw_if_index, self.pg2.sw_if_index, is_l2=1, state=2) + + self.logger.info(self.vapi.ppcli("show interface span")) + # Enable packet capturing and start packet sending + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Verify packets outgoing packet streams on mirrored interface (pg2) + self.logger.info("Verifying capture on interfaces %s and %s" % + (self.pg1.name, self.pg2.name)) + pg2_expected = self.get_packet_count_for_if_idx(self.pg1.sw_if_index) + pg1_pkts = self.pg1.get_capture() + pg2_pkts = self.pg2.get_capture(pg2_expected) + self.verify_capture( + self.pg1, + pg1_pkts, + pg2_pkts) + + self.bridge(self.pg2.sw_if_index, is_add=0) + # Disable SPAN on pg0 (mirrored to pg2) + self.vapi.sw_interface_span_enable_disable( + self.pg1.sw_if_index, self.pg2.sw_if_index, state=0, is_l2=1) + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_srv6.py b/test/test_srv6.py new file mode 100644 index 00000000..a31b30eb --- /dev/null +++ b/test/test_srv6.py @@ -0,0 +1,1997 @@ +#!/usr/bin/env python + +import unittest +from socket import AF_INET6 + +from framework import VppTestCase, VppTestRunner +from vpp_ip_route import VppIpRoute, VppRoutePath, DpoProto +from vpp_srv6 import SRv6LocalSIDBehaviors, VppSRv6LocalSID, VppSRv6Policy, \ + SRv6PolicyType, VppSRv6Steering, SRv6PolicySteeringTypes + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q +from scapy.layers.inet6 import IPv6, UDP, IPv6ExtHdrSegmentRouting +from scapy.layers.inet import IP, UDP + +from scapy.utils import inet_pton, inet_ntop + +from util import ppp + + +class TestSRv6(VppTestCase): + """ SRv6 Test Case """ + + @classmethod + def setUpClass(self): + super(TestSRv6, self).setUpClass() + + def setUp(self): + """ Perform test setup before each test case. + """ + super(TestSRv6, self).setUp() + + # packet sizes, inclusive L2 overhead + self.pg_packet_sizes = [64, 512, 1518, 9018] + + # reset packet_infos + self.reset_packet_infos() + + def tearDown(self): + """ Clean up test setup after each test case. + """ + self.teardown_interfaces() + + super(TestSRv6, self).tearDown() + + def configure_interface(self, + interface, + ipv6=False, ipv4=False, + ipv6_table_id=0, ipv4_table_id=0): + """ Configure interface. + :param ipv6: configure IPv6 on interface + :param ipv4: configure IPv4 on interface + :param ipv6_table_id: FIB table_id for IPv6 + :param ipv4_table_id: FIB table_id for IPv4 + """ + self.logger.debug("Configuring interface %s" % (interface.name)) + if ipv6: + self.logger.debug("Configuring IPv6") + interface.set_table_ip6(ipv6_table_id) + interface.config_ip6() + interface.resolve_ndp(timeout=5) + if ipv4: + self.logger.debug("Configuring IPv4") + interface.set_table_ip4(ipv4_table_id) + interface.config_ip4() + interface.resolve_arp() + interface.admin_up() + + def setup_interfaces(self, ipv6=[], ipv4=[], + ipv6_table_id=[], ipv4_table_id=[]): + """ Create and configure interfaces. + + :param ipv6: list of interface IPv6 capabilities + :param ipv4: list of interface IPv4 capabilities + :param ipv6_table_id: list of intf IPv6 FIB table_ids + :param ipv4_table_id: list of intf IPv4 FIB table_ids + :returns: List of created interfaces. + """ + # how many interfaces? + if len(ipv6): + count = len(ipv6) + else: + count = len(ipv4) + self.logger.debug("Creating and configuring %d interfaces" % (count)) + + # fill up ipv6 and ipv4 lists if needed + # not enabled (False) is the default + if len(ipv6) < count: + ipv6 += (count - len(ipv6)) * [False] + if len(ipv4) < count: + ipv4 += (count - len(ipv4)) * [False] + + # fill up table_id lists if needed + # table_id 0 (global) is the default + if len(ipv6_table_id) < count: + ipv6_table_id += (count - len(ipv6_table_id)) * [0] + if len(ipv4_table_id) < count: + ipv4_table_id += (count - len(ipv4_table_id)) * [0] + + # create 'count' pg interfaces + self.create_pg_interfaces(range(count)) + + # setup all interfaces + for i in range(count): + intf = self.pg_interfaces[i] + self.configure_interface(intf, + ipv6[i], ipv4[i], + ipv6_table_id[i], ipv4_table_id[i]) + + if any(ipv6): + self.logger.debug(self.vapi.cli("show ip6 neighbors")) + if any(ipv4): + self.logger.debug(self.vapi.cli("show ip arp")) + self.logger.debug(self.vapi.cli("show interface")) + self.logger.debug(self.vapi.cli("show hardware")) + + return self.pg_interfaces + + def teardown_interfaces(self): + """ Unconfigure and bring down interface. + """ + self.logger.debug("Tearing down interfaces") + # tear down all interfaces + # AFAIK they cannot be deleted + for i in self.pg_interfaces: + self.logger.debug("Tear down interface %s" % (i.name)) + i.admin_down() + i.unconfig() + + def test_SRv6_T_Encaps(self): + """ Test SRv6 Transit.Encaps behavior for IPv6. + """ + # send traffic to one destination interface + # source and destination are IPv6 only + self.setup_interfaces(ipv6=[True, True]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure encaps IPv6 source address + # needs to be done before SR Policy config + # TODO: API? + self.vapi.cli("set sr encaps source addr a3::") + + bsid = 'a3::9999:1' + # configure SRv6 Policy + # Note: segment list order: first -> last + sr_policy = VppSRv6Policy( + self, bsid=bsid, + is_encap=1, + sr_type=SRv6PolicyType.SR_POLICY_TYPE_DEFAULT, + weight=1, fib_table=0, + segments=['a4::', 'a5::', 'a6::c7'], + source='a3::') + sr_policy.add_vpp_config() + self.sr_policy = sr_policy + + # log the sr policies + self.logger.info(self.vapi.cli("show sr policies")) + + # steer IPv6 traffic to a7::/64 into SRv6 Policy + # use the bsid of the above self.sr_policy + pol_steering = VppSRv6Steering( + self, + bsid=self.sr_policy.bsid, + prefix="a7::", mask_width=64, + traffic_type=SRv6PolicySteeringTypes.SR_STEER_IPV6, + sr_policy_index=0, table_id=0, + sw_if_index=0) + pol_steering.add_vpp_config() + + # log the sr steering policies + self.logger.info(self.vapi.cli("show sr steering policies")) + + # create packets + count = len(self.pg_packet_sizes) + dst_inner = 'a7::1234' + pkts = [] + + # create IPv6 packets without SRH + packet_header = self.create_packet_header_IPv6(dst_inner) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # create IPv6 packets with SRH + # packets with segments-left 1, active segment a7:: + packet_header = self.create_packet_header_IPv6_SRH( + sidlist=['a8::', 'a7::', 'a6::'], + segleft=1) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # create IPv6 packets with SRH and IPv6 + # packets with segments-left 1, active segment a7:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a8::', 'a7::', 'a6::'], + segleft=1) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_T_Encaps) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SR steering + pol_steering.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr steering policies")) + + # remove SR Policies + self.sr_policy.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr policies")) + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_T_Insert(self): + """ Test SRv6 Transit.Insert behavior (IPv6 only). + """ + # send traffic to one destination interface + # source and destination are IPv6 only + self.setup_interfaces(ipv6=[True, True]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure encaps IPv6 source address + # needs to be done before SR Policy config + # TODO: API? + self.vapi.cli("set sr encaps source addr a3::") + + bsid = 'a3::9999:1' + # configure SRv6 Policy + # Note: segment list order: first -> last + sr_policy = VppSRv6Policy( + self, bsid=bsid, + is_encap=0, + sr_type=SRv6PolicyType.SR_POLICY_TYPE_DEFAULT, + weight=1, fib_table=0, + segments=['a4::', 'a5::', 'a6::c7'], + source='a3::') + sr_policy.add_vpp_config() + self.sr_policy = sr_policy + + # log the sr policies + self.logger.info(self.vapi.cli("show sr policies")) + + # steer IPv6 traffic to a7::/64 into SRv6 Policy + # use the bsid of the above self.sr_policy + pol_steering = VppSRv6Steering( + self, + bsid=self.sr_policy.bsid, + prefix="a7::", mask_width=64, + traffic_type=SRv6PolicySteeringTypes.SR_STEER_IPV6, + sr_policy_index=0, table_id=0, + sw_if_index=0) + pol_steering.add_vpp_config() + + # log the sr steering policies + self.logger.info(self.vapi.cli("show sr steering policies")) + + # create packets + count = len(self.pg_packet_sizes) + dst_inner = 'a7::1234' + pkts = [] + + # create IPv6 packets without SRH + packet_header = self.create_packet_header_IPv6(dst_inner) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # create IPv6 packets with SRH + # packets with segments-left 1, active segment a7:: + packet_header = self.create_packet_header_IPv6_SRH( + sidlist=['a8::', 'a7::', 'a6::'], + segleft=1) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_T_Insert) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SR steering + pol_steering.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr steering policies")) + + # remove SR Policies + self.sr_policy.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr policies")) + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_T_Encaps_IPv4(self): + """ Test SRv6 Transit.Encaps behavior for IPv4. + """ + # send traffic to one destination interface + # source interface is IPv4 only + # destination interface is IPv6 only + self.setup_interfaces(ipv6=[False, True], ipv4=[True, False]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure encaps IPv6 source address + # needs to be done before SR Policy config + # TODO: API? + self.vapi.cli("set sr encaps source addr a3::") + + bsid = 'a3::9999:1' + # configure SRv6 Policy + # Note: segment list order: first -> last + sr_policy = VppSRv6Policy( + self, bsid=bsid, + is_encap=1, + sr_type=SRv6PolicyType.SR_POLICY_TYPE_DEFAULT, + weight=1, fib_table=0, + segments=['a4::', 'a5::', 'a6::c7'], + source='a3::') + sr_policy.add_vpp_config() + self.sr_policy = sr_policy + + # log the sr policies + self.logger.info(self.vapi.cli("show sr policies")) + + # steer IPv4 traffic to 7.1.1.0/24 into SRv6 Policy + # use the bsid of the above self.sr_policy + pol_steering = VppSRv6Steering( + self, + bsid=self.sr_policy.bsid, + prefix="7.1.1.0", mask_width=24, + traffic_type=SRv6PolicySteeringTypes.SR_STEER_IPV4, + sr_policy_index=0, table_id=0, + sw_if_index=0) + pol_steering.add_vpp_config() + + # log the sr steering policies + self.logger.info(self.vapi.cli("show sr steering policies")) + + # create packets + count = len(self.pg_packet_sizes) + dst_inner = '7.1.1.123' + pkts = [] + + # create IPv4 packets + packet_header = self.create_packet_header_IPv4(dst_inner) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_T_Encaps_IPv4) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SR steering + pol_steering.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr steering policies")) + + # remove SR Policies + self.sr_policy.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr policies")) + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + @unittest.skip("VPP crashes after running this test") + def test_SRv6_T_Encaps_L2(self): + """ Test SRv6 Transit.Encaps behavior for L2. + """ + # send traffic to one destination interface + # source interface is IPv4 only TODO? + # destination interface is IPv6 only + self.setup_interfaces(ipv6=[False, True], ipv4=[False, False]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure encaps IPv6 source address + # needs to be done before SR Policy config + # TODO: API? + self.vapi.cli("set sr encaps source addr a3::") + + bsid = 'a3::9999:1' + # configure SRv6 Policy + # Note: segment list order: first -> last + sr_policy = VppSRv6Policy( + self, bsid=bsid, + is_encap=1, + sr_type=SRv6PolicyType.SR_POLICY_TYPE_DEFAULT, + weight=1, fib_table=0, + segments=['a4::', 'a5::', 'a6::c7'], + source='a3::') + sr_policy.add_vpp_config() + self.sr_policy = sr_policy + + # log the sr policies + self.logger.info(self.vapi.cli("show sr policies")) + + # steer L2 traffic into SRv6 Policy + # use the bsid of the above self.sr_policy + pol_steering = VppSRv6Steering( + self, + bsid=self.sr_policy.bsid, + prefix="::", mask_width=0, + traffic_type=SRv6PolicySteeringTypes.SR_STEER_L2, + sr_policy_index=0, table_id=0, + sw_if_index=self.pg0.sw_if_index) + pol_steering.add_vpp_config() + + # log the sr steering policies + self.logger.info(self.vapi.cli("show sr steering policies")) + + # create packets + count = len(self.pg_packet_sizes) + pkts = [] + + # create L2 packets without dot1q header + packet_header = self.create_packet_header_L2() + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # create L2 packets with dot1q header + packet_header = self.create_packet_header_L2(vlan=123) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_T_Encaps_L2) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SR steering + pol_steering.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr steering policies")) + + # remove SR Policies + self.sr_policy.remove_vpp_config() + self.logger.info(self.vapi.cli("show sr policies")) + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End(self): + """ Test SRv6 End (without PSP) behavior. + """ + # send traffic to one destination interface + # source and destination interfaces are IPv6 only + self.setup_interfaces(ipv6=[True, True]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure SRv6 localSID End without PSP behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='A3::0', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_END, + nh_addr='::', + end_psp=0, + sw_if_index=0, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=2, SL=1, SL=0) + # send one packet per SL value per packet size + # SL=0 packet with localSID End with USP needs 2nd SRH + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' + pkts = [] + + # packets with segments-left 2, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a5::', 'a4::', 'a3::'], + segleft=2) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets with segments-left 1, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a4::', 'a3::', 'a2::'], + segleft=1) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # TODO: test behavior with SL=0 packet (needs 2*SRH?) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_with_PSP(self): + """ Test SRv6 End with PSP behavior. + """ + # send traffic to one destination interface + # source and destination interfaces are IPv6 only + self.setup_interfaces(ipv6=[True, True]) + + # configure FIB entries + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure SRv6 localSID End with PSP behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='A3::0', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_END, + nh_addr='::', + end_psp=1, + sw_if_index=0, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=2, SL=1) + # send one packet per SL value per packet size + # SL=0 packet with localSID End with PSP is dropped + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' + pkts = [] + + # packets with segments-left 2, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a5::', 'a4::', 'a3::'], + segleft=2) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets with segments-left 1, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a4::', 'a3::', 'a2::'], + segleft=1) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End_PSP) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_X(self): + """ Test SRv6 End.X (without PSP) behavior. + """ + # create three interfaces (1 source, 2 destinations) + # source and destination interfaces are IPv6 only + self.setup_interfaces(ipv6=[True, True, True]) + + # configure FIB entries + # a4::/64 via pg1 and pg2 + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + self.logger.debug(self.vapi.cli("show ip6 fib")) + + # configure SRv6 localSID End.X without PSP behavior + # End.X points to interface pg1 + localsid = VppSRv6LocalSID( + self, localsid_addr='A3::C4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_X, + nh_addr=self.pg1.remote_ip6, + end_psp=0, + sw_if_index=self.pg1.sw_if_index, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=2, SL=1) + # send one packet per SL value per packet size + # SL=0 packet with localSID End with PSP is dropped + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' + pkts = [] + + # packets with segments-left 2, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a5::', 'a4::', 'a3::c4'], + segleft=2) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets with segments-left 1, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a4::', 'a3::c4', 'a2::'], + segleft=1) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + # using same comparison function as End (no PSP) + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End) + + # assert nothing was received on the other interface (pg2) + self.pg2.assert_nothing_captured("mis-directed packet(s)") + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_X_with_PSP(self): + """ Test SRv6 End.X with PSP behavior. + """ + # create three interfaces (1 source, 2 destinations) + # source and destination interfaces are IPv6 only + self.setup_interfaces(ipv6=[True, True, True]) + + # configure FIB entries + # a4::/64 via pg1 and pg2 + route = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6), + VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6)], + is_ip6=1) + route.add_vpp_config() + + # configure SRv6 localSID End with PSP behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='A3::C4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_X, + nh_addr=self.pg1.remote_ip6, + end_psp=1, + sw_if_index=self.pg1.sw_if_index, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=2, SL=1) + # send one packet per SL value per packet size + # SL=0 packet with localSID End with PSP is dropped + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' + pkts = [] + + # packets with segments-left 2, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a5::', 'a4::', 'a3::c4'], + segleft=2) + # create traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets with segments-left 1, active segment a3:: + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a4::', 'a3::c4', 'a2::'], + segleft=1) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + # using same comparison function as End with PSP + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End_PSP) + + # assert nothing was received on the other interface (pg2) + self.pg2.assert_nothing_captured("mis-directed packet(s)") + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_DX6(self): + """ Test SRv6 End.DX6 behavior. + """ + # send traffic to one destination interface + # source and destination interfaces are IPv6 only + self.setup_interfaces(ipv6=[True, True]) + + # configure SRv6 localSID End.DX6 behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='a3::c4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_DX6, + nh_addr=self.pg1.remote_ip6, + end_psp=0, + sw_if_index=self.pg1.sw_if_index, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=0) + # send one packet per packet size + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' # inner header destination address + pkts = [] + + # packets with SRH, segments-left 0, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, IPv6 in IPv6 + # outer IPv6 dest addr is the localsid End.DX6 + packet_header = self.create_packet_header_IPv6_IPv6( + dst_inner, + dst_outer='a3::c4') + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End_DX6) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_DT6(self): + """ Test SRv6 End.DT6 behavior. + """ + # create three interfaces (1 source, 2 destinations) + # all interfaces are IPv6 only + # source interface in global FIB (0) + # destination interfaces in global and vrf + vrf_1 = 1 + self.setup_interfaces(ipv6=[True, True, True], + ipv6_table_id=[0, 0, vrf_1]) + + # configure FIB entries + # a4::/64 is reachable + # via pg1 in table 0 (global) + # and via pg2 in table vrf_1 + route0 = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg1.remote_ip6, + self.pg1.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6, + nh_table_id=0)], + table_id=0, + is_ip6=1) + route0.add_vpp_config() + route1 = VppIpRoute(self, "a4::", 64, + [VppRoutePath(self.pg2.remote_ip6, + self.pg2.sw_if_index, + proto=DpoProto.DPO_PROTO_IP6, + nh_table_id=vrf_1)], + table_id=vrf_1, + is_ip6=1) + route1.add_vpp_config() + self.logger.debug(self.vapi.cli("show ip6 fib")) + + # configure SRv6 localSID End.DT6 behavior + # Note: + # fib_table: where the localsid is installed + # sw_if_index: in T-variants of localsid this is the vrf table_id + localsid = VppSRv6LocalSID( + self, localsid_addr='a3::c4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_DT6, + nh_addr='::', + end_psp=0, + sw_if_index=vrf_1, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=0) + # send one packet per packet size + count = len(self.pg_packet_sizes) + dst_inner = 'a4::1234' # inner header destination address + pkts = [] + + # packets with SRH, segments-left 0, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv6( + dst_inner, + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg2, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, IPv6 in IPv6 + # outer IPv6 dest addr is the localsid End.DT6 + packet_header = self.create_packet_header_IPv6_IPv6( + dst_inner, + dst_outer='a3::c4') + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg2, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + # using same comparison function as End.DX6 + self.send_and_verify_pkts(self.pg0, pkts, self.pg2, + self.compare_rx_tx_packet_End_DX6) + + # assert nothing was received on the other interface (pg2) + self.pg1.assert_nothing_captured("mis-directed packet(s)") + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_DX4(self): + """ Test SRv6 End.DX4 behavior. + """ + # send traffic to one destination interface + # source interface is IPv6 only + # destination interface is IPv4 only + self.setup_interfaces(ipv6=[True, False], ipv4=[False, True]) + + # configure SRv6 localSID End.DX4 behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='a3::c4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_DX4, + nh_addr=self.pg1.remote_ip4, + end_psp=0, + sw_if_index=self.pg1.sw_if_index, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # send one packet per packet size + count = len(self.pg_packet_sizes) + dst_inner = '4.1.1.123' # inner header destination address + pkts = [] + + # packets with SRH, segments-left 0, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv4( + dst_inner, + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, IPv4 in IPv6 + # outer IPv6 dest addr is the localsid End.DX4 + packet_header = self.create_packet_header_IPv6_IPv4( + dst_inner, + dst_outer='a3::c4') + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End_DX4) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_DT4(self): + """ Test SRv6 End.DT4 behavior. + """ + # create three interfaces (1 source, 2 destinations) + # source interface is IPv6-only + # destination interfaces are IPv4 only + # source interface in global FIB (0) + # destination interfaces in global and vrf + vrf_1 = 1 + self.setup_interfaces(ipv6=[True, False, False], + ipv4=[False, True, True], + ipv6_table_id=[0, 0, 0], + ipv4_table_id=[0, 0, vrf_1]) + + # configure FIB entries + # 4.1.1.0/24 is reachable + # via pg1 in table 0 (global) + # and via pg2 in table vrf_1 + route0 = VppIpRoute(self, "4.1.1.0", 24, + [VppRoutePath(self.pg1.remote_ip4, + self.pg1.sw_if_index, + nh_table_id=0)], + table_id=0, + is_ip6=0) + route0.add_vpp_config() + route1 = VppIpRoute(self, "4.1.1.0", 24, + [VppRoutePath(self.pg2.remote_ip4, + self.pg2.sw_if_index, + nh_table_id=vrf_1)], + table_id=vrf_1, + is_ip6=0) + route1.add_vpp_config() + self.logger.debug(self.vapi.cli("show ip fib")) + + # configure SRv6 localSID End.DT6 behavior + # Note: + # fib_table: where the localsid is installed + # sw_if_index: in T-variants of localsid: vrf table_id + localsid = VppSRv6LocalSID( + self, localsid_addr='a3::c4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_DT4, + nh_addr='::', + end_psp=0, + sw_if_index=vrf_1, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # create IPv6 packets with SRH (SL=0) + # send one packet per packet size + count = len(self.pg_packet_sizes) + dst_inner = '4.1.1.123' # inner header destination address + pkts = [] + + # packets with SRH, segments-left 0, active segment a3::c4 + packet_header = self.create_packet_header_IPv6_SRH_IPv4( + dst_inner, + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg2, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, IPv6 in IPv6 + # outer IPv6 dest addr is the localsid End.DX4 + packet_header = self.create_packet_header_IPv6_IPv4( + dst_inner, + dst_outer='a3::c4') + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg2, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + # using same comparison function as End.DX4 + self.send_and_verify_pkts(self.pg0, pkts, self.pg2, + self.compare_rx_tx_packet_End_DX4) + + # assert nothing was received on the other interface (pg2) + self.pg1.assert_nothing_captured("mis-directed packet(s)") + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # remove FIB entries + # done by tearDown + + # cleanup interfaces + self.teardown_interfaces() + + def test_SRv6_End_DX2(self): + """ Test SRv6 End.DX2 behavior. + """ + # send traffic to one destination interface + # source interface is IPv6 only + self.setup_interfaces(ipv6=[True, False], ipv4=[False, False]) + + # configure SRv6 localSID End.DX2 behavior + localsid = VppSRv6LocalSID( + self, localsid_addr='a3::c4', + behavior=SRv6LocalSIDBehaviors.SR_BEHAVIOR_DX2, + nh_addr='::', + end_psp=0, + sw_if_index=self.pg1.sw_if_index, + vlan_index=0, + fib_table=0) + localsid.add_vpp_config() + # log the localsids + self.logger.debug(self.vapi.cli("show sr localsid")) + + # send one packet per packet size + count = len(self.pg_packet_sizes) + pkts = [] + + # packets with SRH, segments-left 0, active segment a3::c4 + # L2 has no dot1q header + packet_header = self.create_packet_header_IPv6_SRH_L2( + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0, + vlan=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets with SRH, segments-left 0, active segment a3::c4 + # L2 has dot1q header + packet_header = self.create_packet_header_IPv6_SRH_L2( + sidlist=['a3::c4', 'a2::', 'a1::'], + segleft=0, + vlan=123) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, L2 in IPv6 + # outer IPv6 dest addr is the localsid End.DX2 + # L2 has no dot1q header + packet_header = self.create_packet_header_IPv6_L2( + dst_outer='a3::c4', + vlan=0) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # packets without SRH, L2 in IPv6 + # outer IPv6 dest addr is the localsid End.DX2 + # L2 has dot1q header + packet_header = self.create_packet_header_IPv6_L2( + dst_outer='a3::c4', + vlan=123) + # add to traffic stream pg0->pg1 + pkts.extend(self.create_stream(self.pg0, self.pg1, packet_header, + self.pg_packet_sizes, count)) + + # send packets and verify received packets + self.send_and_verify_pkts(self.pg0, pkts, self.pg1, + self.compare_rx_tx_packet_End_DX2) + + # log the localsid counters + self.logger.info(self.vapi.cli("show sr localsid")) + + # remove SRv6 localSIDs + localsid.remove_vpp_config() + + # cleanup interfaces + self.teardown_interfaces() + + def compare_rx_tx_packet_T_Encaps(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing T.Encaps + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # T.Encaps updates the headers as follows: + # SR Policy seglist (S3, S2, S1) + # SR Policy source C + # IPv6: + # in: IPv6(A, B2) + # out: IPv6(C, S1)SRH(S3, S2, S1; SL=2)IPv6(A, B2) + # IPv6 + SRH: + # in: IPv6(A, B2)SRH(B3, B2, B1; SL=1) + # out: IPv6(C, S1)SRH(S3, S2, S1; SL=2)IPv6(a, B2)SRH(B3, B2, B1; SL=1) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + + tx_ip = tx_pkt.getlayer(IPv6) + + # expected segment-list + seglist = self.sr_policy.segments + # reverse list to get order as in SRH + tx_seglist = seglist[::-1] + + # get source address of SR Policy + sr_policy_source = self.sr_policy.source + + # rx'ed packet should have SRH + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + # get SRH + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # received ip.src should be equal to SR Policy source + self.assertEqual(rx_ip.src, sr_policy_source) + # received ip.dst should be equal to expected sidlist[lastentry] + self.assertEqual(rx_ip.dst, tx_seglist[-1]) + # rx'ed seglist should be equal to expected seglist + self.assertEqual(rx_srh.addresses, tx_seglist) + # segleft should be equal to size expected seglist-1 + self.assertEqual(rx_srh.segleft, len(tx_seglist)-1) + # segleft should be equal to lastentry + self.assertEqual(rx_srh.segleft, rx_srh.lastentry) + + # the whole rx'ed pkt beyond SRH should be equal to tx'ed pkt + # except for the hop-limit field + # -> update tx'ed hlim to the expected hlim + tx_ip.hlim = tx_ip.hlim - 1 + + self.assertEqual(rx_srh.payload, tx_ip) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_T_Encaps_IPv4(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing T.Encaps for IPv4 + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # T.Encaps for IPv4 updates the headers as follows: + # SR Policy seglist (S3, S2, S1) + # SR Policy source C + # IPv4: + # in: IPv4(A, B2) + # out: IPv6(C, S1)SRH(S3, S2, S1; SL=2)IPv4(A, B2) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + + tx_ip = tx_pkt.getlayer(IP) + + # expected segment-list + seglist = self.sr_policy.segments + # reverse list to get order as in SRH + tx_seglist = seglist[::-1] + + # get source address of SR Policy + sr_policy_source = self.sr_policy.source + + # checks common to cases tx with and without SRH + # rx'ed packet should have SRH and IPv4 header + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + self.assertTrue(rx_ip.payload.haslayer(IP)) + # get SRH + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # received ip.src should be equal to SR Policy source + self.assertEqual(rx_ip.src, sr_policy_source) + # received ip.dst should be equal to sidlist[lastentry] + self.assertEqual(rx_ip.dst, tx_seglist[-1]) + # rx'ed seglist should be equal to seglist + self.assertEqual(rx_srh.addresses, tx_seglist) + # segleft should be equal to size seglist-1 + self.assertEqual(rx_srh.segleft, len(tx_seglist)-1) + # segleft should be equal to lastentry + self.assertEqual(rx_srh.segleft, rx_srh.lastentry) + + # the whole rx'ed pkt beyond SRH should be equal to tx'ed pkt + # except for the ttl field and ip checksum + # -> adjust tx'ed ttl to expected ttl + tx_ip.ttl = tx_ip.ttl - 1 + # -> set tx'ed ip checksum to None and let scapy recompute + tx_ip.chksum = None + # read back the pkt (with str()) to force computing these fields + # probably other ways to accomplish this are possible + tx_ip = IP(str(tx_ip)) + + self.assertEqual(rx_srh.payload, tx_ip) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_T_Encaps_L2(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing T.Encaps for L2 + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # T.Encaps for L2 updates the headers as follows: + # SR Policy seglist (S3, S2, S1) + # SR Policy source C + # L2: + # in: L2 + # out: IPv6(C, S1)SRH(S3, S2, S1; SL=2)L2 + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + + tx_ether = tx_pkt.getlayer(Ether) + + # expected segment-list + seglist = self.sr_policy.segments + # reverse list to get order as in SRH + tx_seglist = seglist[::-1] + + # get source address of SR Policy + sr_policy_source = self.sr_policy.source + + # rx'ed packet should have SRH + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + # get SRH + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # received ip.src should be equal to SR Policy source + self.assertEqual(rx_ip.src, sr_policy_source) + # received ip.dst should be equal to sidlist[lastentry] + self.assertEqual(rx_ip.dst, tx_seglist[-1]) + # rx'ed seglist should be equal to seglist + self.assertEqual(rx_srh.addresses, tx_seglist) + # segleft should be equal to size seglist-1 + self.assertEqual(rx_srh.segleft, len(tx_seglist)-1) + # segleft should be equal to lastentry + self.assertEqual(rx_srh.segleft, rx_srh.lastentry) + # nh should be "No Next Header" (59) + self.assertEqual(rx_srh.nh, 59) + + # the whole rx'ed pkt beyond SRH should be equal to tx'ed pkt + self.assertEqual(Ether(str(rx_srh.payload)), tx_ether) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_T_Insert(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing T.Insert + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # T.Insert updates the headers as follows: + # IPv6: + # in: IPv6(A, B2) + # out: IPv6(A, S1)SRH(B2, S3, S2, S1; SL=3) + # IPv6 + SRH: + # in: IPv6(A, B2)SRH(B3, B2, B1; SL=1) + # out: IPv6(A, S1)SRH(B2, S3, S2, S1; SL=3)SRH(B3, B2, B1; SL=1) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + rx_ip2 = None + rx_srh2 = None + rx_ip3 = None + rx_udp = rx_pkt[UDP] + + tx_ip = tx_pkt.getlayer(IPv6) + tx_srh = None + tx_ip2 = None + # some packets have been tx'ed with an SRH, some without it + # get SRH if tx'ed packet has it + if tx_pkt.haslayer(IPv6ExtHdrSegmentRouting): + tx_srh = tx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + tx_ip2 = tx_pkt.getlayer(IPv6, 2) + tx_udp = tx_pkt[UDP] + + # expected segment-list (make copy of SR Policy segment list) + seglist = self.sr_policy.segments[:] + # expected seglist has initial dest addr as last segment + seglist.append(tx_ip.dst) + # reverse list to get order as in SRH + tx_seglist = seglist[::-1] + + # get source address of SR Policy + sr_policy_source = self.sr_policy.source + + # checks common to cases tx with and without SRH + # rx'ed packet should have SRH and only one IPv6 header + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + self.assertFalse(rx_ip.payload.haslayer(IPv6)) + # get SRH + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # rx'ed ip.src should be equal to tx'ed ip.src + self.assertEqual(rx_ip.src, tx_ip.src) + # rx'ed ip.dst should be equal to sidlist[lastentry] + self.assertEqual(rx_ip.dst, tx_seglist[-1]) + + # rx'ed seglist should be equal to expected seglist + self.assertEqual(rx_srh.addresses, tx_seglist) + # segleft should be equal to size(expected seglist)-1 + self.assertEqual(rx_srh.segleft, len(tx_seglist)-1) + # segleft should be equal to lastentry + self.assertEqual(rx_srh.segleft, rx_srh.lastentry) + + if tx_srh: # packet was tx'ed with SRH + # packet should have 2nd SRH + self.assertTrue(rx_srh.payload.haslayer(IPv6ExtHdrSegmentRouting)) + # get 2nd SRH + rx_srh2 = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting, 2) + + # rx'ed srh2.addresses should be equal to tx'ed srh.addresses + self.assertEqual(rx_srh2.addresses, tx_srh.addresses) + # rx'ed srh2.segleft should be equal to tx'ed srh.segleft + self.assertEqual(rx_srh2.segleft, tx_srh.segleft) + # rx'ed srh2.lastentry should be equal to tx'ed srh.lastentry + self.assertEqual(rx_srh2.lastentry, tx_srh.lastentry) + + else: # packet was tx'ed without SRH + # rx packet should have no other SRH + self.assertFalse(rx_srh.payload.haslayer(IPv6ExtHdrSegmentRouting)) + + # UDP layer should be unchanged + self.assertEqual(rx_udp, tx_udp) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_End(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing End (without PSP) + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # End (no PSP) updates the headers as follows: + # IPv6 + SRH: + # in: IPv6(A, S1)SRH(S3, S2, S1; SL=2) + # out: IPv6(A, S2)SRH(S3, S2, S1; SL=1) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + rx_ip2 = None + rx_udp = rx_pkt[UDP] + + tx_ip = tx_pkt.getlayer(IPv6) + # we know the packet has been tx'ed + # with an inner IPv6 header and an SRH + tx_ip2 = tx_pkt.getlayer(IPv6, 2) + tx_srh = tx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + tx_udp = tx_pkt[UDP] + + # common checks, regardless of tx segleft value + # rx'ed packet should have 2nd IPv6 header + self.assertTrue(rx_ip.payload.haslayer(IPv6)) + # get second (inner) IPv6 header + rx_ip2 = rx_pkt.getlayer(IPv6, 2) + + if tx_ip.segleft > 0: + # SRH should NOT have been popped: + # End SID without PSP does not pop SRH if segleft>0 + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # received ip.src should be equal to expected ip.src + self.assertEqual(rx_ip.src, tx_ip.src) + # sidlist should be unchanged + self.assertEqual(rx_srh.addresses, tx_srh.addresses) + # segleft should have been decremented + self.assertEqual(rx_srh.segleft, tx_srh.segleft-1) + # received ip.dst should be equal to sidlist[segleft] + self.assertEqual(rx_ip.dst, rx_srh.addresses[rx_srh.segleft]) + # lastentry should be unchanged + self.assertEqual(rx_srh.lastentry, tx_srh.lastentry) + # inner IPv6 packet (ip2) should be unchanged + self.assertEqual(rx_ip2.src, tx_ip2.src) + self.assertEqual(rx_ip2.dst, tx_ip2.dst) + # else: # tx_ip.segleft == 0 + # TODO: Does this work with 2 SRHs in ingress packet? + + # UDP layer should be unchanged + self.assertEqual(rx_udp, tx_udp) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_End_PSP(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing End with PSP + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # End (PSP) updates the headers as follows: + # IPv6 + SRH (SL>1): + # in: IPv6(A, S1)SRH(S3, S2, S1; SL=2) + # out: IPv6(A, S2)SRH(S3, S2, S1; SL=1) + # IPv6 + SRH (SL=1): + # in: IPv6(A, S2)SRH(S3, S2, S1; SL=1) + # out: IPv6(A, S3) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + rx_srh = None + rx_ip2 = None + rx_udp = rx_pkt[UDP] + + tx_ip = tx_pkt.getlayer(IPv6) + # we know the packet has been tx'ed + # with an inner IPv6 header and an SRH + tx_ip2 = tx_pkt.getlayer(IPv6, 2) + tx_srh = tx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + tx_udp = tx_pkt[UDP] + + # common checks, regardless of tx segleft value + self.assertTrue(rx_ip.payload.haslayer(IPv6)) + rx_ip2 = rx_pkt.getlayer(IPv6, 2) + # inner IPv6 packet (ip2) should be unchanged + self.assertEqual(rx_ip2.src, tx_ip2.src) + self.assertEqual(rx_ip2.dst, tx_ip2.dst) + + if tx_ip.segleft > 1: + # SRH should NOT have been popped: + # End SID with PSP does not pop SRH if segleft>1 + # rx'ed packet should have SRH + self.assertTrue(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + rx_srh = rx_pkt.getlayer(IPv6ExtHdrSegmentRouting) + + # received ip.src should be equal to expected ip.src + self.assertEqual(rx_ip.src, tx_ip.src) + # sidlist should be unchanged + self.assertEqual(rx_srh.addresses, tx_srh.addresses) + # segleft should have been decremented + self.assertEqual(rx_srh.segleft, tx_srh.segleft-1) + # received ip.dst should be equal to sidlist[segleft] + self.assertEqual(rx_ip.dst, rx_srh.addresses[rx_srh.segleft]) + # lastentry should be unchanged + self.assertEqual(rx_srh.lastentry, tx_srh.lastentry) + + else: # tx_ip.segleft <= 1 + # SRH should have been popped: + # End SID with PSP and segleft=1 pops SRH + # the two IPv6 headers are still present + # outer IPv6 header has DA == last segment of popped SRH + # SRH should not be present + self.assertFalse(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + # outer IPv6 header ip.src should be equal to tx'ed ip.src + self.assertEqual(rx_ip.src, tx_ip.src) + # outer IPv6 header ip.dst should be = to tx'ed sidlist[segleft-1] + self.assertEqual(rx_ip.dst, tx_srh.addresses[tx_srh.segleft-1]) + + # UDP layer should be unchanged + self.assertEqual(rx_udp, tx_udp) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_End_DX6(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing End.DX6 + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # End.DX6 updates the headers as follows: + # IPv6 + SRH (SL=0): + # in: IPv6(A, S3)SRH(S3, S2, S1; SL=0)IPv6(B, D) + # out: IPv6(B, D) + # IPv6: + # in: IPv6(A, S3)IPv6(B, D) + # out: IPv6(B, D) + + # get first (outer) IPv6 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IPv6) + + tx_ip = tx_pkt.getlayer(IPv6) + tx_ip2 = tx_pkt.getlayer(IPv6, 2) + + # verify if rx'ed packet has no SRH + self.assertFalse(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + + # the whole rx_ip pkt should be equal to tx_ip2 + # except for the hlim field + # -> adjust tx'ed hlim to expected hlim + tx_ip2.hlim = tx_ip2.hlim - 1 + + self.assertEqual(rx_ip, tx_ip2) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_End_DX4(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing End.DX4 + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # End.DX4 updates the headers as follows: + # IPv6 + SRH (SL=0): + # in: IPv6(A, S3)SRH(S3, S2, S1; SL=0)IPv4(B, D) + # out: IPv4(B, D) + # IPv6: + # in: IPv6(A, S3)IPv4(B, D) + # out: IPv4(B, D) + + # get IPv4 header of rx'ed packet + rx_ip = rx_pkt.getlayer(IP) + + tx_ip = tx_pkt.getlayer(IPv6) + tx_ip2 = tx_pkt.getlayer(IP) + + # verify if rx'ed packet has no SRH + self.assertFalse(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + + # the whole rx_ip pkt should be equal to tx_ip2 + # except for the ttl field and ip checksum + # -> adjust tx'ed ttl to expected ttl + tx_ip2.ttl = tx_ip2.ttl - 1 + # -> set tx'ed ip checksum to None and let scapy recompute + tx_ip2.chksum = None + # read back the pkt (with str()) to force computing these fields + # probably other ways to accomplish this are possible + tx_ip2 = IP(str(tx_ip2)) + + self.assertEqual(rx_ip, tx_ip2) + + self.logger.debug("packet verification: SUCCESS") + + def compare_rx_tx_packet_End_DX2(self, tx_pkt, rx_pkt): + """ Compare input and output packet after passing End.DX2 + + :param tx_pkt: transmitted packet + :param rx_pkt: received packet + """ + # End.DX2 updates the headers as follows: + # IPv6 + SRH (SL=0): + # in: IPv6(A, S3)SRH(S3, S2, S1; SL=0)L2 + # out: L2 + # IPv6: + # in: IPv6(A, S3)L2 + # out: L2 + + # get IPv4 header of rx'ed packet + rx_eth = rx_pkt.getlayer(Ether) + + tx_ip = tx_pkt.getlayer(IPv6) + # we can't just get the 2nd Ether layer + # get the Raw content and dissect it as Ether + tx_eth1 = Ether(str(tx_pkt[Raw])) + + # verify if rx'ed packet has no SRH + self.assertFalse(rx_pkt.haslayer(IPv6ExtHdrSegmentRouting)) + + # the whole rx_eth pkt should be equal to tx_eth1 + self.assertEqual(rx_eth, tx_eth1) + + self.logger.debug("packet verification: SUCCESS") + + def create_stream(self, src_if, dst_if, packet_header, packet_sizes, + count): + """Create SRv6 input packet stream for defined interface. + + :param VppInterface src_if: Interface to create packet stream for + :param VppInterface dst_if: destination interface of packet stream + :param packet_header: Layer3 scapy packet headers, + L2 is added when not provided, + Raw(payload) with packet_info is added + :param list packet_sizes: packet stream pckt sizes,sequentially applied + to packets in stream have + :param int count: number of packets in packet stream + :return: list of packets + """ + self.logger.info("Creating packets") + pkts = [] + for i in range(0, count-1): + payload_info = self.create_packet_info(src_if, dst_if) + self.logger.debug( + "Creating packet with index %d" % (payload_info.index)) + payload = self.info_to_payload(payload_info) + # add L2 header if not yet provided in packet_header + if packet_header.getlayer(0).name == 'Ethernet': + p = (packet_header / + Raw(payload)) + else: + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + packet_header / + Raw(payload)) + size = packet_sizes[i % len(packet_sizes)] + self.logger.debug("Packet size %d" % (size)) + self.extend_packet(p, size) + # we need to store the packet with the automatic fields computed + # read back the dumped packet (with str()) + # to force computing these fields + # probably other ways are possible + p = Ether(str(p)) + payload_info.data = p.copy() + self.logger.debug(ppp("Created packet:", p)) + pkts.append(p) + self.logger.info("Done creating packets") + return pkts + + def send_and_verify_pkts(self, input, pkts, output, compare_func): + """Send packets and verify received packets using compare_func + + :param input: ingress interface of DUT + :param pkts: list of packets to transmit + :param output: egress interface of DUT + :param compare_func: function to compare in and out packets + """ + # add traffic stream to input interface + input.add_stream(pkts) + + # enable capture on all interfaces + self.pg_enable_capture(self.pg_interfaces) + + # start traffic + self.logger.info("Starting traffic") + self.pg_start() + + # get output capture + self.logger.info("Getting packet capture") + capture = output.get_capture() + + # assert nothing was captured on input interface + input.assert_nothing_captured() + + # verify captured packets + self.verify_captured_pkts(output, capture, compare_func) + + def create_packet_header_IPv6(self, dst): + """Create packet header: IPv6 header, UDP header + + :param dst: IPv6 destination address + + IPv6 source address is 1234::1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=dst) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_SRH(self, sidlist, segleft): + """Create packet header: IPv6 header with SRH, UDP header + + :param list sidlist: segment list + :param int segleft: segments-left field value + + IPv6 destination address is set to sidlist[segleft] + IPv6 source addresses are 1234::1 and 4321::1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=sidlist[segleft]) / + IPv6ExtHdrSegmentRouting(addresses=sidlist) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_SRH_IPv6(self, dst, sidlist, segleft): + """Create packet header: IPv6 encapsulated in SRv6: + IPv6 header with SRH, IPv6 header, UDP header + + :param ipv6address dst: inner IPv6 destination address + :param list sidlist: segment list of outer IPv6 SRH + :param int segleft: segments-left field of outer IPv6 SRH + + Outer IPv6 destination address is set to sidlist[segleft] + IPv6 source addresses are 1234::1 and 4321::1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=sidlist[segleft]) / + IPv6ExtHdrSegmentRouting(addresses=sidlist, + segleft=segleft, nh=41) / + IPv6(src='4321::1', dst=dst) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_IPv6(self, dst_inner, dst_outer): + """Create packet header: IPv6 encapsulated in IPv6: + IPv6 header, IPv6 header, UDP header + + :param ipv6address dst_inner: inner IPv6 destination address + :param ipv6address dst_outer: outer IPv6 destination address + + IPv6 source addresses are 1234::1 and 4321::1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=dst_outer) / + IPv6(src='4321::1', dst=dst_inner) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_SRH_SRH_IPv6(self, dst, sidlist1, segleft1, + sidlist2, segleft2): + """Create packet header: IPv6 encapsulated in SRv6 with 2 SRH: + IPv6 header with SRH, 2nd SRH, IPv6 header, UDP header + + :param ipv6address dst: inner IPv6 destination address + :param list sidlist1: segment list of outer IPv6 SRH + :param int segleft1: segments-left field of outer IPv6 SRH + :param list sidlist2: segment list of inner IPv6 SRH + :param int segleft2: segments-left field of inner IPv6 SRH + + Outer IPv6 destination address is set to sidlist[segleft] + IPv6 source addresses are 1234::1 and 4321::1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=sidlist1[segleft1]) / + IPv6ExtHdrSegmentRouting(addresses=sidlist1, + segleft=segleft1, nh=43) / + IPv6ExtHdrSegmentRouting(addresses=sidlist2, + segleft=segleft2, nh=41) / + IPv6(src='4321::1', dst=dst) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv4(self, dst): + """Create packet header: IPv4 header, UDP header + + :param dst: IPv4 destination address + + IPv4 source address is 123.1.1.1 + UDP source port and destination port are 1234 + """ + + p = (IP(src='123.1.1.1', dst=dst) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_IPv4(self, dst_inner, dst_outer): + """Create packet header: IPv4 encapsulated in IPv6: + IPv6 header, IPv4 header, UDP header + + :param ipv4address dst_inner: inner IPv4 destination address + :param ipv6address dst_outer: outer IPv6 destination address + + IPv6 source address is 1234::1 + IPv4 source address is 123.1.1.1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=dst_outer) / + IP(src='123.1.1.1', dst=dst_inner) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_IPv6_SRH_IPv4(self, dst, sidlist, segleft): + """Create packet header: IPv4 encapsulated in SRv6: + IPv6 header with SRH, IPv4 header, UDP header + + :param ipv4address dst: inner IPv4 destination address + :param list sidlist: segment list of outer IPv6 SRH + :param int segleft: segments-left field of outer IPv6 SRH + + Outer IPv6 destination address is set to sidlist[segleft] + IPv6 source address is 1234::1 + IPv4 source address is 123.1.1.1 + UDP source port and destination port are 1234 + """ + + p = (IPv6(src='1234::1', dst=sidlist[segleft]) / + IPv6ExtHdrSegmentRouting(addresses=sidlist, + segleft=segleft, nh=4) / + IP(src='123.1.1.1', dst=dst) / + UDP(sport=1234, dport=1234)) + return p + + def create_packet_header_L2(self, vlan=0): + """Create packet header: L2 header + + :param vlan: if vlan!=0 then add 802.1q header + """ + # Note: the dst addr ('00:55:44:33:22:11') is used in + # the compare function compare_rx_tx_packet_T_Encaps_L2 + # to detect presence of L2 in SRH payload + p = Ether(src='00:11:22:33:44:55', dst='00:55:44:33:22:11') + etype = 0x8137 # IPX + if vlan: + # add 802.1q layer + p /= Dot1Q(vlan=vlan, type=etype) + else: + p.type = etype + return p + + def create_packet_header_IPv6_SRH_L2(self, sidlist, segleft, vlan=0): + """Create packet header: L2 encapsulated in SRv6: + IPv6 header with SRH, L2 + + :param list sidlist: segment list of outer IPv6 SRH + :param int segleft: segments-left field of outer IPv6 SRH + :param vlan: L2 vlan; if vlan!=0 then add 802.1q header + + Outer IPv6 destination address is set to sidlist[segleft] + IPv6 source address is 1234::1 + """ + eth = Ether(src='00:11:22:33:44:55', dst='00:55:44:33:22:11') + etype = 0x8137 # IPX + if vlan: + # add 802.1q layer + eth /= Dot1Q(vlan=vlan, type=etype) + else: + eth.type = etype + + p = (IPv6(src='1234::1', dst=sidlist[segleft]) / + IPv6ExtHdrSegmentRouting(addresses=sidlist, + segleft=segleft, nh=59) / + eth) + return p + + def create_packet_header_IPv6_L2(self, dst_outer, vlan=0): + """Create packet header: L2 encapsulated in IPv6: + IPv6 header, L2 + + :param ipv6address dst_outer: outer IPv6 destination address + :param vlan: L2 vlan; if vlan!=0 then add 802.1q header + """ + eth = Ether(src='00:11:22:33:44:55', dst='00:55:44:33:22:11') + etype = 0x8137 # IPX + if vlan: + # add 802.1q layer + eth /= Dot1Q(vlan=vlan, type=etype) + else: + eth.type = etype + + p = (IPv6(src='1234::1', dst=dst_outer, nh=59) / eth) + return p + + def get_payload_info(self, packet): + """ Extract the payload_info from the packet + """ + # in most cases, payload_info is in packet[Raw] + # but packet[Raw] gives the complete payload + # (incl L2 header) for the T.Encaps L2 case + try: + payload_info = self.payload_to_info(str(packet[Raw])) + + except: + # remote L2 header from packet[Raw]: + # take packet[Raw], convert it to an Ether layer + # and then extract Raw from it + payload_info = self.payload_to_info( + str(Ether(str(packet[Raw]))[Raw])) + + return payload_info + + def verify_captured_pkts(self, dst_if, capture, compare_func): + """ + Verify captured packet stream for specified interface. + Compare ingress with egress packets using the specified compare fn + + :param dst_if: egress interface of DUT + :param capture: captured packets + :param compare_func: function to compare in and out packet + """ + self.logger.info("Verifying capture on interface %s using function %s" + % (dst_if.name, compare_func.func_name)) + + last_info = dict() + for i in self.pg_interfaces: + last_info[i.sw_if_index] = None + dst_sw_if_index = dst_if.sw_if_index + + for packet in capture: + try: + # extract payload_info from packet's payload + payload_info = self.get_payload_info(packet) + packet_index = payload_info.index + + self.logger.debug("Verifying packet with index %d" + % (packet_index)) + # packet should have arrived on the expected interface + self.assertEqual(payload_info.dst, dst_sw_if_index) + self.logger.debug( + "Got packet on interface %s: src=%u (idx=%u)" % + (dst_if.name, payload_info.src, packet_index)) + + # search for payload_info with same src and dst if_index + # this will give us the transmitted packet + next_info = self.get_next_packet_info_for_interface2( + payload_info.src, dst_sw_if_index, + last_info[payload_info.src]) + last_info[payload_info.src] = next_info + # next_info should not be None + self.assertTrue(next_info is not None) + # index of tx and rx packets should be equal + self.assertEqual(packet_index, next_info.index) + # data field of next_info contains the tx packet + txed_packet = next_info.data + + self.logger.debug(ppp("Transmitted packet:", + txed_packet)) # ppp=Pretty Print Packet + + self.logger.debug(ppp("Received packet:", packet)) + + # compare rcvd packet with expected packet using compare_func + compare_func(txed_packet, packet) + + except: + print packet.command() + self.logger.error(ppp("Unexpected or invalid packet:", packet)) + raise + + # have all expected packets arrived? + for i in self.pg_interfaces: + remaining_packet = self.get_next_packet_info_for_interface2( + i.sw_if_index, dst_sw_if_index, last_info[i.sw_if_index]) + self.assertTrue(remaining_packet is None, + "Interface %s: Packet expected from interface %s " + "didn't arrive" % (dst_if.name, i.name)) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_vapi.py b/test/test_vapi.py new file mode 100644 index 00000000..d8e1ebe0 --- /dev/null +++ b/test/test_vapi.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +""" VAPI test """ + +from __future__ import division +import unittest +import os +import signal +import subprocess +from threading import Thread +from log import single_line_delim +from framework import VppTestCase, running_extended_tests, VppTestRunner + + +class Worker(Thread): + def __init__(self, args, logger): + self.logger = logger + self.args = args + self.result = None + super(Worker, self).__init__() + + def run(self): + executable = self.args[0] + self.logger.debug("Running executable w/args `%s'" % self.args) + env = os.environ.copy() + env["CK_LOG_FILE_NAME"] = "-" + self.process = subprocess.Popen( + self.args, shell=False, env=env, preexec_fn=os.setpgrp, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = self.process.communicate() + self.logger.debug("Finished running `%s'" % executable) + self.logger.info("Return code is `%s'" % self.process.returncode) + self.logger.info(single_line_delim) + self.logger.info("Executable `%s' wrote to stdout:" % executable) + self.logger.info(single_line_delim) + self.logger.info(out) + self.logger.info(single_line_delim) + self.logger.info("Executable `%s' wrote to stderr:" % executable) + self.logger.info(single_line_delim) + self.logger.error(err) + self.logger.info(single_line_delim) + self.result = self.process.returncode + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class VAPITestCase(VppTestCase): + """ VAPI test """ + + def test_vapi_c(self): + """ run C VAPI tests """ + var = "BR" + built_root = os.getenv(var, None) + self.assertIsNotNone(built_root, + "Environment variable `%s' not set" % var) + executable = "%s/vapi_test/vapi_c_test" % built_root + worker = Worker( + [executable, "vapi client", self.shm_prefix], self.logger) + worker.start() + timeout = 60 + worker.join(timeout) + self.logger.info("Worker result is `%s'" % worker.result) + error = False + if worker.result is None: + try: + error = True + self.logger.error( + "Timeout! Worker did not finish in %ss" % timeout) + os.killpg(os.getpgid(worker.process.pid), signal.SIGTERM) + worker.join() + except: + raise Exception("Couldn't kill worker-spawned process") + if error: + raise Exception( + "Timeout! Worker did not finish in %ss" % timeout) + self.assert_equal(worker.result, 0, "Binary test return code") + + def test_vapi_cpp(self): + """ run C++ VAPI tests """ + var = "BR" + built_root = os.getenv(var, None) + self.assertIsNotNone(built_root, + "Environment variable `%s' not set" % var) + executable = "%s/vapi_test/vapi_cpp_test" % built_root + worker = Worker( + [executable, "vapi client", self.shm_prefix], self.logger) + worker.start() + timeout = 120 + worker.join(timeout) + self.logger.info("Worker result is `%s'" % worker.result) + error = False + if worker.result is None: + try: + error = True + self.logger.error( + "Timeout! Worker did not finish in %ss" % timeout) + os.killpg(os.getpgid(worker.process.pid), signal.SIGTERM) + worker.join() + except: + raise Exception("Couldn't kill worker-spawned process") + if error: + raise Exception( + "Timeout! Worker did not finish in %ss" % timeout) + self.assert_equal(worker.result, 0, "Binary test return code") + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_vtr.py b/test/test_vtr.py new file mode 100644 index 00000000..02df2ce2 --- /dev/null +++ b/test/test_vtr.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python + +import unittest +import random + +from scapy.packet import Raw +from scapy.layers.l2 import Ether, Dot1Q +from scapy.layers.inet import IP, UDP + +from util import Host +from framework import VppTestCase, VppTestRunner +from vpp_sub_interface import VppDot1QSubint, VppDot1ADSubint +from vpp_papi_provider import L2_VTR_OP +from collections import namedtuple + +Tag = namedtuple('Tag', ['dot1', 'vlan']) +DOT1AD = 0x88A8 +DOT1Q = 0x8100 + + +class TestVtr(VppTestCase): + """ VTR Test Case """ + + @classmethod + def setUpClass(cls): + super(TestVtr, cls).setUpClass() + + # Test variables + cls.bd_id = 1 + cls.mac_entries_count = 5 + cls.Atag = 100 + cls.Btag = 200 + cls.dot1ad_sub_id = 20 + + try: + ifs = range(3) + cls.create_pg_interfaces(ifs) + + cls.sub_interfaces = [ + VppDot1ADSubint(cls, cls.pg1, cls.dot1ad_sub_id, + cls.Btag, cls.Atag), + VppDot1QSubint(cls, cls.pg2, cls.Btag)] + + interfaces = list(cls.pg_interfaces) + interfaces.extend(cls.sub_interfaces) + + # Create BD with MAC learning enabled and put interfaces and + # sub-interfaces to this BD + for pg_if in cls.pg_interfaces: + sw_if_index = pg_if.sub_if.sw_if_index \ + if hasattr(pg_if, 'sub_if') else pg_if.sw_if_index + cls.vapi.sw_interface_set_l2_bridge(sw_if_index, + bd_id=cls.bd_id) + + # setup all interfaces + for i in interfaces: + i.admin_up() + + # mapping between packet-generator index and lists of test hosts + cls.hosts_by_pg_idx = dict() + + # create test host entries and inject packets to learn MAC entries + # in the bridge-domain + cls.create_hosts_and_learn(cls.mac_entries_count) + cls.logger.info(cls.vapi.ppcli("show l2fib")) + + except Exception: + super(TestVtr, cls).tearDownClass() + raise + + def setUp(self): + """ + Clear trace and packet infos before running each test. + """ + super(TestVtr, self).setUp() + self.reset_packet_infos() + + def tearDown(self): + """ + Show various debug prints after each test. + """ + super(TestVtr, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.ppcli("show l2fib verbose")) + self.logger.info(self.vapi.ppcli("show bridge-domain %s detail" % + self.bd_id)) + + @classmethod + def create_hosts_and_learn(cls, count): + for pg_if in cls.pg_interfaces: + cls.hosts_by_pg_idx[pg_if.sw_if_index] = [] + hosts = cls.hosts_by_pg_idx[pg_if.sw_if_index] + packets = [] + for j in range(1, count + 1): + host = Host( + "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j), + "172.17.1%02x.%u" % (pg_if.sw_if_index, j)) + packet = (Ether(dst="ff:ff:ff:ff:ff:ff", src=host.mac)) + hosts.append(host) + if hasattr(pg_if, 'sub_if'): + packet = pg_if.sub_if.add_dot1_layer(packet) + packets.append(packet) + pg_if.add_stream(packets) + cls.logger.info("Sending broadcast eth frames for MAC learning") + cls.pg_enable_capture(cls.pg_interfaces) + cls.pg_start() + + def create_packet(self, src_if, dst_if, do_dot1=True): + packet_sizes = [64, 512, 1518, 9018] + dst_host = random.choice(self.hosts_by_pg_idx[dst_if.sw_if_index]) + src_host = random.choice(self.hosts_by_pg_idx[src_if.sw_if_index]) + pkt_info = self.create_packet_info(src_if, dst_if) + payload = self.info_to_payload(pkt_info) + p = (Ether(dst=dst_host.mac, src=src_host.mac) / + IP(src=src_host.ip4, dst=dst_host.ip4) / + UDP(sport=1234, dport=1234) / + Raw(payload)) + pkt_info.data = p.copy() + if do_dot1 and hasattr(src_if, 'sub_if'): + p = src_if.sub_if.add_dot1_layer(p) + size = random.choice(packet_sizes) + self.extend_packet(p, size) + return p + + def _add_tag(self, packet, vlan, tag_type): + payload = packet.payload + inner_type = packet.type + packet.remove_payload() + packet.add_payload(Dot1Q(vlan=vlan) / payload) + packet.payload.type = inner_type + packet.payload.vlan = vlan + packet.type = tag_type + return packet + + def _remove_tag(self, packet, vlan=None, tag_type=None): + if tag_type: + self.assertEqual(packet.type, tag_type) + + payload = packet.payload + if vlan: + self.assertEqual(payload.vlan, vlan) + inner_type = payload.type + payload = payload.payload + packet.remove_payload() + packet.add_payload(payload) + packet.type = inner_type + + def add_tags(self, packet, tags): + for t in reversed(tags): + self._add_tag(packet, t.vlan, t.dot1) + + def remove_tags(self, packet, tags): + for t in tags: + self._remove_tag(packet, t.vlan, t.dot1) + + def vtr_test(self, swif, tags): + p = self.create_packet(swif, self.pg0) + swif.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = self.pg0.get_capture(1) + + if tags: + self.remove_tags(rx[0], tags) + self.assertTrue(Dot1Q not in rx[0]) + + if not tags: + return + + i = VppDot1QSubint(self, self.pg0, tags[0].vlan) + self.vapi.sw_interface_set_l2_bridge( + i.sw_if_index, bd_id=self.bd_id, enable=1) + i.admin_up() + + p = self.create_packet(self.pg0, swif, do_dot1=False) + self.add_tags(p, tags) + self.pg0.add_stream(p) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + rx = swif.get_capture(1) + swif.sub_if.remove_dot1_layer(rx[0]) + self.assertTrue(Dot1Q not in rx[0]) + + self.vapi.sw_interface_set_l2_bridge( + i.sw_if_index, bd_id=self.bd_id, enable=0) + i.remove_vpp_config() + + def test_1ad_vtr_pop_1(self): + """ 1AD VTR pop 1 test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_POP_1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_pop_2(self): + """ 1AD VTR pop 2 test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_POP_2) + self.vtr_test(self.pg1, []) + + def test_1ad_vtr_push_1ad(self): + """ 1AD VTR push 1 1AD test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_1, tag=300) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=300), + Tag(dot1=DOT1AD, vlan=200), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_push_2ad(self): + """ 1AD VTR push 2 1AD test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_2, outer=400, inner=300) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=400), + Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1AD, vlan=200), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_push_1q(self): + """ 1AD VTR push 1 1Q test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_1, tag=300, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1AD, vlan=200), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_push_2q(self): + """ 1AD VTR push 2 1Q test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_2, + outer=400, inner=300, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=400), + Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1AD, vlan=200), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_translate_1_1ad(self): + """ 1AD VTR translate 1 -> 1 1AD test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_TRANSLATE_1_1, tag=300) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=300), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_translate_1_2ad(self): + """ 1AD VTR translate 1 -> 2 1AD test + """ + self.pg1.sub_if.set_vtr( + L2_VTR_OP.L2_TRANSLATE_1_2, inner=300, outer=400) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=400), + Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_translate_2_1ad(self): + """ 1AD VTR translate 2 -> 1 1AD test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_TRANSLATE_2_1, tag=300) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=300)]) + + def test_1ad_vtr_translate_2_2ad(self): + """ 1AD VTR translate 2 -> 2 1AD test + """ + self.pg1.sub_if.set_vtr( + L2_VTR_OP.L2_TRANSLATE_2_2, inner=300, outer=400) + self.vtr_test(self.pg1, [Tag(dot1=DOT1AD, vlan=400), + Tag(dot1=DOT1Q, vlan=300)]) + + def test_1ad_vtr_translate_1_1q(self): + """ 1AD VTR translate 1 -> 1 1Q test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_TRANSLATE_1_1, tag=300, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_translate_1_2q(self): + """ 1AD VTR translate 1 -> 2 1Q test + """ + self.pg1.sub_if.set_vtr( + L2_VTR_OP.L2_TRANSLATE_1_2, inner=300, outer=400, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=400), + Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1Q, vlan=100)]) + + def test_1ad_vtr_translate_2_1q(self): + """ 1AD VTR translate 2 -> 1 1Q test + """ + self.pg1.sub_if.set_vtr(L2_VTR_OP.L2_TRANSLATE_2_1, tag=300, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=300)]) + + def test_1ad_vtr_translate_2_2q(self): + """ 1AD VTR translate 2 -> 2 1Q test + """ + self.pg1.sub_if.set_vtr( + L2_VTR_OP.L2_TRANSLATE_2_2, inner=300, outer=400, push1q=1) + self.vtr_test(self.pg1, [Tag(dot1=DOT1Q, vlan=400), + Tag(dot1=DOT1Q, vlan=300)]) + + def test_1q_vtr_pop_1(self): + """ 1Q VTR pop 1 test + """ + self.pg2.sub_if.set_vtr(L2_VTR_OP.L2_POP_1) + self.vtr_test(self.pg2, []) + + def test_1q_vtr_push_1(self): + """ 1Q VTR push 1 test + """ + self.pg2.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_1, tag=300) + self.vtr_test(self.pg2, [Tag(dot1=DOT1AD, vlan=300), + Tag(dot1=DOT1Q, vlan=200)]) + + def test_1q_vtr_push_2(self): + """ 1Q VTR push 2 test + """ + self.pg2.sub_if.set_vtr(L2_VTR_OP.L2_PUSH_2, outer=400, inner=300) + self.vtr_test(self.pg2, [Tag(dot1=DOT1AD, vlan=400), + Tag(dot1=DOT1Q, vlan=300), + Tag(dot1=DOT1Q, vlan=200)]) + + def test_1q_vtr_translate_1_1(self): + """ 1Q VTR translate 1 -> 1 test + """ + self.pg2.sub_if.set_vtr(L2_VTR_OP.L2_TRANSLATE_1_1, tag=300) + self.vtr_test(self.pg2, [Tag(dot1=DOT1AD, vlan=300)]) + + def test_1q_vtr_translate_1_2(self): + """ 1Q VTR translate 1 -> 2 test + """ + self.pg2.sub_if.set_vtr( + L2_VTR_OP.L2_TRANSLATE_1_2, inner=300, outer=400) + self.vtr_test(self.pg2, [Tag(dot1=DOT1AD, vlan=400), + Tag(dot1=DOT1Q, vlan=300)]) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_vxlan.py b/test/test_vxlan.py new file mode 100644 index 00000000..6bdcb258 --- /dev/null +++ b/test/test_vxlan.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python + +import socket +from util import ip4n_range +import unittest +from framework import VppTestCase, VppTestRunner +from template_bd import BridgeDomain + +from scapy.layers.l2 import Ether +from scapy.layers.inet import IP, UDP +from scapy.layers.vxlan import VXLAN +from scapy.utils import atol + + +class TestVxlan(BridgeDomain, VppTestCase): + """ VXLAN Test Case """ + + def __init__(self, *args): + BridgeDomain.__init__(self) + VppTestCase.__init__(self, *args) + + def encapsulate(self, pkt, vni): + """ + Encapsulate the original payload frame by adding VXLAN header with its + UDP, IP and Ethernet fields + """ + return (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + VXLAN(vni=vni, flags=self.flags) / + pkt) + + def encap_mcast(self, pkt, src_ip, src_mac, vni): + """ + Encapsulate the original payload frame by adding VXLAN header with its + UDP, IP and Ethernet fields + """ + return (Ether(src=src_mac, dst=self.mcast_mac) / + IP(src=src_ip, dst=self.mcast_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + VXLAN(vni=vni, flags=self.flags) / + pkt) + + def decapsulate(self, pkt): + """ + Decapsulate the original payload frame by removing VXLAN header + """ + # check if is set I flag + self.assertEqual(pkt[VXLAN].flags, int('0x8', 16)) + return pkt[VXLAN].payload + + # Method for checking VXLAN encapsulation. + # + def check_encapsulation(self, pkt, vni, local_only=False, mcast_pkt=False): + # TODO: add error messages + # Verify source MAC is VPP_MAC and destination MAC is MY_MAC resolved + # by VPP using ARP. + self.assertEqual(pkt[Ether].src, self.pg0.local_mac) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[Ether].dst, self.pg0.remote_mac) + else: + self.assertEqual(pkt[Ether].dst, type(self).mcast_mac) + # Verify VXLAN tunnel source IP is VPP_IP and destination IP is MY_IP. + self.assertEqual(pkt[IP].src, self.pg0.local_ip4) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[IP].dst, self.pg0.remote_ip4) + else: + self.assertEqual(pkt[IP].dst, type(self).mcast_ip4) + # Verify UDP destination port is VXLAN 4789, source UDP port could be + # arbitrary. + self.assertEqual(pkt[UDP].dport, type(self).dport) + # TODO: checksum check + # Verify VNI + self.assertEqual(pkt[VXLAN].vni, vni) + + @classmethod + def create_vxlan_flood_test_bd(cls, vni, n_ucast_tunnels): + # Create 10 ucast vxlan tunnels under bd + ip_range_start = 10 + ip_range_end = ip_range_start + n_ucast_tunnels + next_hop_address = cls.pg0.remote_ip4n + for dest_ip4n in ip4n_range(next_hop_address, ip_range_start, + ip_range_end): + # add host route so dest_ip4n will not be resolved + cls.vapi.ip_add_del_route(dest_ip4n, 32, next_hop_address) + r = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + vni=vni) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, bd_id=vni) + + @classmethod + def add_del_shared_mcast_dst_load(cls, is_add): + """ + add or del tunnels sharing the same mcast dst + to test vxlan ref_count mechanism + """ + n_shared_dst_tunnels = 2000 + vni_start = 10000 + vni_end = vni_start + n_shared_dst_tunnels + for vni in range(vni_start, vni_end): + r = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + vni=vni, + is_add=is_add) + if r.sw_if_index == 0xffffffff: + raise "bad sw_if_index" + + @classmethod + def add_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=1) + + @classmethod + def del_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=0) + + @classmethod + def add_del_mcast_tunnels_load(cls, is_add): + """ + add or del tunnels to test vxlan stability + """ + n_distinct_dst_tunnels = 200 + ip_range_start = 10 + ip_range_end = ip_range_start + n_distinct_dst_tunnels + for dest_ip4n in ip4n_range(cls.mcast_ip4n, ip_range_start, + ip_range_end): + vni = bytearray(dest_ip4n)[3] + cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + mcast_sw_if_index=1, + vni=vni, + is_add=is_add) + + @classmethod + def add_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=1) + + @classmethod + def del_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=0) + + # Class method to start the VXLAN test case. + # Overrides setUpClass method in VppTestCase class. + # Python try..except statement is used to ensure that the tear down of + # the class will be executed even if exception is raised. + # @param cls The class pointer. + @classmethod + def setUpClass(cls): + super(TestVxlan, cls).setUpClass() + + try: + cls.dport = 4789 + cls.flags = 0x8 + + # Create 2 pg interfaces. + cls.create_pg_interfaces(range(4)) + for pg in cls.pg_interfaces: + pg.admin_up() + + # Configure IPv4 addresses on VPP pg0. + cls.pg0.config_ip4() + + # Resolve MAC address for VPP's IP address on pg0. + cls.pg0.resolve_arp() + + # Our Multicast address + cls.mcast_ip4 = '239.1.1.1' + cls.mcast_ip4n = socket.inet_pton(socket.AF_INET, cls.mcast_ip4) + iplong = atol(cls.mcast_ip4) + cls.mcast_mac = "01:00:5e:%02x:%02x:%02x" % ( + (iplong >> 16) & 0x7F, (iplong >> 8) & 0xFF, iplong & 0xFF) + + # Create VXLAN VTEP on VPP pg0, and put vxlan_tunnel0 and pg1 + # into BD. + cls.single_tunnel_bd = 1 + r = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.pg0.remote_ip4n, + vni=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg1.sw_if_index, + bd_id=cls.single_tunnel_bd) + + # Setup vni 2 to test multicast flooding + cls.n_ucast_tunnels = 10 + cls.mcast_flood_bd = 2 + cls.create_vxlan_flood_test_bd(cls.mcast_flood_bd, + cls.n_ucast_tunnels) + r = cls.vapi.vxlan_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + vni=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg2.sw_if_index, + bd_id=cls.mcast_flood_bd) + + # Add and delete mcast tunnels to check stability + cls.add_shared_mcast_dst_load() + cls.add_mcast_tunnels_load() + cls.del_shared_mcast_dst_load() + cls.del_mcast_tunnels_load() + + # Setup vni 3 to test unicast flooding + cls.ucast_flood_bd = 3 + cls.create_vxlan_flood_test_bd(cls.ucast_flood_bd, + cls.n_ucast_tunnels) + cls.vapi.sw_interface_set_l2_bridge(cls.pg3.sw_if_index, + bd_id=cls.ucast_flood_bd) + except Exception: + super(TestVxlan, cls).tearDownClass() + raise + + # Method to define VPP actions before tear down of the test case. + # Overrides tearDown method in VppTestCase class. + # @param self The object pointer. + def tearDown(self): + super(TestVxlan, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show bridge-domain 1 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 2 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 3 detail")) + self.logger.info(self.vapi.cli("show vxlan tunnel")) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/test_vxlan_gpe.py b/test/test_vxlan_gpe.py new file mode 100644 index 00000000..40975bf1 --- /dev/null +++ b/test/test_vxlan_gpe.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python + +import socket +from util import ip4n_range +import unittest +from framework import VppTestCase, VppTestRunner, running_extended_tests +from template_bd import BridgeDomain + +from scapy.layers.l2 import Ether, Raw +from scapy.layers.inet import IP, UDP +from scapy.layers.vxlan import VXLAN +from scapy.utils import atol + + +@unittest.skipUnless(running_extended_tests(), "part of extended tests") +class TestVxlanGpe(BridgeDomain, VppTestCase): + """ VXLAN-GPE Test Case """ + + def __init__(self, *args): + BridgeDomain.__init__(self) + VppTestCase.__init__(self, *args) + + def encapsulate(self, pkt, vni): + """ + Encapsulate the original payload frame by adding VXLAN-GPE header + with its UDP, IP and Ethernet fields + """ + return (Ether(src=self.pg0.remote_mac, dst=self.pg0.local_mac) / + IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + VXLAN(vni=vni, flags=self.flags) / + pkt) + + def encap_mcast(self, pkt, src_ip, src_mac, vni): + """ + Encapsulate the original payload frame by adding VXLAN-GPE header + with its UDP, IP and Ethernet fields + """ + return (Ether(src=src_mac, dst=self.mcast_mac) / + IP(src=src_ip, dst=self.mcast_ip4) / + UDP(sport=self.dport, dport=self.dport, chksum=0) / + VXLAN(vni=vni, flags=self.flags) / + pkt) + + def decapsulate(self, pkt): + """ + Decapsulate the original payload frame by removing VXLAN-GPE header + """ + # check if is set I and P flag + self.assertEqual(pkt[VXLAN].flags, int('0xc', 16)) + return pkt[VXLAN].payload + + # Method for checking VXLAN-GPE encapsulation. + # + def check_encapsulation(self, pkt, vni, local_only=False, mcast_pkt=False): + # Verify source MAC is VPP_MAC and destination MAC is MY_MAC resolved + # by VPP using ARP. + self.assertEqual(pkt[Ether].src, self.pg0.local_mac) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[Ether].dst, self.pg0.remote_mac) + else: + self.assertEqual(pkt[Ether].dst, type(self).mcast_mac) + # Verify VXLAN-GPE tunnel src IP is VPP_IP and dst IP is MY_IP. + self.assertEqual(pkt[IP].src, self.pg0.local_ip4) + if not local_only: + if not mcast_pkt: + self.assertEqual(pkt[IP].dst, self.pg0.remote_ip4) + else: + self.assertEqual(pkt[IP].dst, type(self).mcast_ip4) + # Verify UDP destination port is VXLAN-GPE 4790, source UDP port + # could be arbitrary. + self.assertEqual(pkt[UDP].dport, type(self).dport) + # Verify VNI + self.assertEqual(pkt[VXLAN].VNI, vni) + + def test_decap(self): + """ Decapsulation test + Send encapsulated frames from pg0 + Verify receipt of decapsulated frames on pg1 + """ + + encapsulated_pkt = self.encapsulate(self.frame_request, + self.single_tunnel_bd) + + self.pg0.add_stream([encapsulated_pkt, ]) + + self.pg1.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's the non-encapsulated + # frame + out = self.pg1.get_capture(1) + pkt = out[0] + self.assert_eq_pkts(pkt, self.frame_request) + + def test_encap(self): + """ Encapsulation test + Send frames from pg1 + Verify receipt of encapsulated frames on pg0 + """ + self.pg1.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.single_tunnel_bd) + + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + def test_ucast_flood(self): + """ Unicast flood test + Send frames from pg3 + Verify receipt of encapsulated frames on pg0 + """ + self.pg3.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Get packet from each tunnel and assert it's corectly encapsulated. + out = self.pg0.get_capture(self.n_ucast_tunnels) + for pkt in out: + self.check_encapsulation(pkt, self.ucast_flood_bd, True) + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + def test_mcast_flood(self): + """ Multicast flood test + Send frames from pg2 + Verify receipt of encapsulated frames on pg0 + """ + self.pg2.add_stream([self.frame_reply]) + + self.pg0.enable_capture() + + self.pg_start() + + # Pick first received frame and check if it's corectly encapsulated. + out = self.pg0.get_capture(1) + pkt = out[0] + self.check_encapsulation(pkt, self.mcast_flood_bd, + local_only=False, mcast_pkt=True) + + # payload = self.decapsulate(pkt) + # self.assert_eq_pkts(payload, self.frame_reply) + + @classmethod + def create_vxlan_gpe_flood_test_bd(cls, vni, n_ucast_tunnels): + # Create 10 ucast vxlan_gpe tunnels under bd + ip_range_start = 10 + ip_range_end = ip_range_start + n_ucast_tunnels + next_hop_address = cls.pg0.remote_ip4n + for dest_ip4n in ip4n_range(next_hop_address, ip_range_start, + ip_range_end): + # add host route so dest_ip4n will not be resolved + cls.vapi.ip_add_del_route(dest_ip4n, 32, next_hop_address) + r = cls.vapi.vxlan_gpe_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + vni=vni) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, bd_id=vni) + + @classmethod + def add_del_shared_mcast_dst_load(cls, is_add): + """ + add or del tunnels sharing the same mcast dst + to test vxlan_gpe ref_count mechanism + """ + n_shared_dst_tunnels = 20 + vni_start = 1000 + vni_end = vni_start + n_shared_dst_tunnels + for vni in range(vni_start, vni_end): + r = cls.vapi.vxlan_gpe_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + vni=vni, + is_add=is_add) + if r.sw_if_index == 0xffffffff: + raise "bad sw_if_index" + + @classmethod + def add_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=1) + + @classmethod + def del_shared_mcast_dst_load(cls): + cls.add_del_shared_mcast_dst_load(is_add=0) + + @classmethod + def add_del_mcast_tunnels_load(cls, is_add): + """ + add or del tunnels to test vxlan_gpe stability + """ + n_distinct_dst_tunnels = 20 + ip_range_start = 10 + ip_range_end = ip_range_start + n_distinct_dst_tunnels + for dest_ip4n in ip4n_range(cls.mcast_ip4n, ip_range_start, + ip_range_end): + vni = bytearray(dest_ip4n)[3] + cls.vapi.vxlan_gpe_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=dest_ip4n, + mcast_sw_if_index=1, + vni=vni, + is_add=is_add) + + @classmethod + def add_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=1) + + @classmethod + def del_mcast_tunnels_load(cls): + cls.add_del_mcast_tunnels_load(is_add=0) + + # Class method to start the VXLAN-GPE test case. + # Overrides setUpClass method in VppTestCase class. + # Python try..except statement is used to ensure that the tear down of + # the class will be executed even if exception is raised. + # @param cls The class pointer. + @classmethod + def setUpClass(cls): + super(TestVxlanGpe, cls).setUpClass() + + try: + cls.dport = 4790 + cls.flags = 0xc + + # Create 2 pg interfaces. + cls.create_pg_interfaces(range(4)) + for pg in cls.pg_interfaces: + pg.admin_up() + + # Configure IPv4 addresses on VPP pg0. + cls.pg0.config_ip4() + + # Resolve MAC address for VPP's IP address on pg0. + cls.pg0.resolve_arp() + + # Our Multicast address + cls.mcast_ip4 = '239.1.1.1' + cls.mcast_ip4n = socket.inet_pton(socket.AF_INET, cls.mcast_ip4) + iplong = atol(cls.mcast_ip4) + cls.mcast_mac = "01:00:5e:%02x:%02x:%02x" % ( + (iplong >> 16) & 0x7F, (iplong >> 8) & 0xFF, iplong & 0xFF) + + # Create VXLAN-GPE VTEP on VPP pg0, and put vxlan_gpe_tunnel0 + # and pg1 into BD. + cls.single_tunnel_bd = 11 + r = cls.vapi.vxlan_gpe_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.pg0.remote_ip4n, + vni=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.single_tunnel_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg1.sw_if_index, + bd_id=cls.single_tunnel_bd) + + # Setup vni 2 to test multicast flooding + cls.n_ucast_tunnels = 10 + cls.mcast_flood_bd = 12 + cls.create_vxlan_gpe_flood_test_bd(cls.mcast_flood_bd, + cls.n_ucast_tunnels) + r = cls.vapi.vxlan_gpe_add_del_tunnel( + src_addr=cls.pg0.local_ip4n, + dst_addr=cls.mcast_ip4n, + mcast_sw_if_index=1, + vni=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(r.sw_if_index, + bd_id=cls.mcast_flood_bd) + cls.vapi.sw_interface_set_l2_bridge(cls.pg2.sw_if_index, + bd_id=cls.mcast_flood_bd) + + # Add and delete mcast tunnels to check stability + cls.add_shared_mcast_dst_load() + cls.add_mcast_tunnels_load() + cls.del_shared_mcast_dst_load() + cls.del_mcast_tunnels_load() + + # Setup vni 3 to test unicast flooding + cls.ucast_flood_bd = 13 + cls.create_vxlan_gpe_flood_test_bd(cls.ucast_flood_bd, + cls.n_ucast_tunnels) + cls.vapi.sw_interface_set_l2_bridge(cls.pg3.sw_if_index, + bd_id=cls.ucast_flood_bd) + except Exception: + super(TestVxlanGpe, cls).tearDownClass() + raise + + # Method to define VPP actions before tear down of the test case. + # Overrides tearDown method in VppTestCase class. + # @param self The object pointer. + def tearDown(self): + super(TestVxlanGpe, self).tearDown() + if not self.vpp_dead: + self.logger.info(self.vapi.cli("show bridge-domain 11 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 12 detail")) + self.logger.info(self.vapi.cli("show bridge-domain 13 detail")) + self.logger.info(self.vapi.cli("show int")) + self.logger.info(self.vapi.cli("show vxlan-gpe tunnel")) + self.logger.info(self.vapi.cli("show trace")) + + +if __name__ == '__main__': + unittest.main(testRunner=VppTestRunner) diff --git a/test/util.py b/test/util.py new file mode 100644 index 00000000..3e0267a3 --- /dev/null +++ b/test/util.py @@ -0,0 +1,231 @@ +""" test framework utilities """ + +import socket +import sys +from abc import abstractmethod, ABCMeta +from cStringIO import StringIO +from scapy.layers.inet6 import in6_mactoifaceid + +from scapy.layers.l2 import Ether +from scapy.packet import Raw +from scapy.layers.inet import IP, UDP, TCP +from scapy.layers.inet6 import IPv6, ICMPv6Unknown, ICMPv6EchoRequest +from scapy.packet import Packet +from socket import inet_pton, AF_INET, AF_INET6 + + +def ppp(headline, packet): + """ Return string containing the output of scapy packet.show() call. """ + o = StringIO() + old_stdout = sys.stdout + sys.stdout = o + print(headline) + packet.show() + sys.stdout = old_stdout + return o.getvalue() + + +def ppc(headline, capture, limit=10): + """ Return string containing ppp() printout for a capture. + + :param headline: printed as first line of output + :param capture: packets to print + :param limit: limit the print to # of packets + """ + if not capture: + return headline + tail = "" + if limit < len(capture): + tail = "\nPrint limit reached, %s out of %s packets printed" % ( + len(capture), limit) + limit = len(capture) + body = "".join([ppp("Packet #%s:" % count, p) + for count, p in zip(range(0, limit), capture)]) + return "%s\n%s%s" % (headline, body, tail) + + +def ip4_range(ip4, s, e): + tmp = ip4.rsplit('.', 1)[0] + return ("%s.%d" % (tmp, i) for i in range(s, e)) + + +def ip4n_range(ip4n, s, e): + ip4 = socket.inet_ntop(socket.AF_INET, ip4n) + return (socket.inet_pton(socket.AF_INET, ip) + for ip in ip4_range(ip4, s, e)) + + +def mactobinary(mac): + """ Convert the : separated format into binary packet data for the API """ + return mac.replace(':', '').decode('hex') + + +def mk_ll_addr(mac): + euid = in6_mactoifaceid(mac) + addr = "fe80::" + euid + return addr + + +class NumericConstant(object): + __metaclass__ = ABCMeta + + desc_dict = {} + + @abstractmethod + def __init__(self, value): + self._value = value + + def __int__(self): + return self._value + + def __long__(self): + return self._value + + def __str__(self): + if self._value in self.desc_dict: + return self.desc_dict[self._value] + return "" + + +class Host(object): + """ Generic test host "connected" to VPPs interface. """ + + @property + def mac(self): + """ MAC address """ + return self._mac + + @property + def bin_mac(self): + """ MAC address """ + return mactobinary(self._mac) + + @property + def ip4(self): + """ IPv4 address - string """ + return self._ip4 + + @property + def ip4n(self): + """ IPv4 address of remote host - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET, self._ip4) + + @property + def ip6(self): + """ IPv6 address - string """ + return self._ip6 + + @property + def ip6n(self): + """ IPv6 address of remote host - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET6, self._ip6) + + @property + def ip6_ll(self): + """ IPv6 link-local address - string """ + return self._ip6_ll + + @property + def ip6n_ll(self): + """ IPv6 link-local address of remote host - + raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET6, self._ip6_ll) + + def __eq__(self, other): + if isinstance(other, Host): + return (self.mac == other.mac and + self.ip4 == other.ip4 and + self.ip6 == other.ip6 and + self.ip6_ll == other.ip6_ll) + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "Host { mac:%s ip4:%s ip6:%s ip6_ll:%s }" % (self.mac, + self.ip4, + self.ip6, + self.ip6_ll) + + def __hash__(self): + return hash(self.__repr__()) + + def __init__(self, mac=None, ip4=None, ip6=None, ip6_ll=None): + self._mac = mac + self._ip4 = ip4 + self._ip6 = ip6 + self._ip6_ll = ip6_ll + + +class ForeignAddressFactory(object): + count = 0 + prefix_len = 24 + net_template = '10.10.10.{}' + net = net_template.format(0) + '/' + str(prefix_len) + + def get_ip4(self): + if self.count > 255: + raise Exception("Network host address exhaustion") + self.count += 1 + return self.net_template.format(self.count) + + +class L4_Conn(): + """ L4 'connection' tied to two VPP interfaces """ + def __init__(self, testcase, if1, if2, af, l4proto, port1, port2): + self.testcase = testcase + self.ifs = [None, None] + self.ifs[0] = if1 + self.ifs[1] = if2 + self.address_family = af + self.l4proto = l4proto + self.ports = [None, None] + self.ports[0] = port1 + self.ports[1] = port2 + self + + def pkt(self, side, l4args={}, payload="x"): + is_ip6 = 1 if self.address_family == AF_INET6 else 0 + s0 = side + s1 = 1-side + src_if = self.ifs[s0] + dst_if = self.ifs[s1] + layer_3 = [IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4), + IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)] + merged_l4args = {'sport': self.ports[s0], 'dport': self.ports[s1]} + merged_l4args.update(l4args) + p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / + layer_3[is_ip6] / + self.l4proto(**merged_l4args) / + Raw(payload)) + return p + + def send(self, side, flags=None, payload=""): + l4args = {} + if flags is not None: + l4args['flags'] = flags + self.ifs[side].add_stream(self.pkt(side, + l4args=l4args, payload=payload)) + self.ifs[1-side].enable_capture() + self.testcase.pg_start() + + def recv(self, side): + p = self.ifs[side].wait_for_packet(1) + return p + + def send_through(self, side, flags=None, payload=""): + self.send(side, flags, payload) + p = self.recv(1-side) + return p + + def send_pingpong(self, side, flags1=None, flags2=None): + p1 = self.send_through(side, flags1) + p2 = self.send_through(1-side, flags2) + return [p1, p2] + + +class L4_CONN_SIDE: + L4_CONN_SIDE_ZERO = 0 + L4_CONN_SIDE_ONE = 1 diff --git a/test/vpp_gre_interface.py b/test/vpp_gre_interface.py new file mode 100644 index 00000000..1c71875f --- /dev/null +++ b/test/vpp_gre_interface.py @@ -0,0 +1,71 @@ + +from vpp_interface import VppInterface +import socket + + +class VppGreInterface(VppInterface): + """ + VPP GRE interface + """ + + def __init__(self, test, src_ip, dst_ip, outer_fib_id=0, is_teb=0): + """ Create VPP loopback interface """ + self._sw_if_index = 0 + super(VppGreInterface, self).__init__(test) + self._test = test + self.t_src = src_ip + self.t_dst = dst_ip + self.t_outer_fib = outer_fib_id + self.t_is_teb = is_teb + + def add_vpp_config(self): + s = socket.inet_pton(socket.AF_INET, self.t_src) + d = socket.inet_pton(socket.AF_INET, self.t_dst) + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_teb=self.t_is_teb) + self._sw_if_index = r.sw_if_index + self.generate_remote_hosts() + + def remove_vpp_config(self): + s = socket.inet_pton(socket.AF_INET, self.t_src) + d = socket.inet_pton(socket.AF_INET, self.t_dst) + self.unconfig() + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_add=0) + + +class VppGre6Interface(VppInterface): + """ + VPP GRE IPv6 interface + """ + + def __init__(self, test, src_ip, dst_ip, outer_fib_id=0, is_teb=0): + """ Create VPP loopback interface """ + self._sw_if_index = 0 + super(VppGre6Interface, self).__init__(test) + self._test = test + self.t_src = src_ip + self.t_dst = dst_ip + self.t_outer_fib = outer_fib_id + self.t_is_teb = is_teb + + def add_vpp_config(self): + s = socket.inet_pton(socket.AF_INET6, self.t_src) + d = socket.inet_pton(socket.AF_INET6, self.t_dst) + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_teb=self.t_is_teb, + is_ip6=1) + self._sw_if_index = r.sw_if_index + self.generate_remote_hosts() + + def remove_vpp_config(self): + s = socket.inet_pton(socket.AF_INET6, self.t_src) + d = socket.inet_pton(socket.AF_INET6, self.t_dst) + self.unconfig() + r = self.test.vapi.gre_tunnel_add_del(s, d, + outer_fib_id=self.t_outer_fib, + is_add=0, + is_ip6=1) diff --git a/test/vpp_interface.py b/test/vpp_interface.py new file mode 100644 index 00000000..b8505ce3 --- /dev/null +++ b/test/vpp_interface.py @@ -0,0 +1,374 @@ +from abc import abstractmethod, ABCMeta +import socket + +from util import Host, mk_ll_addr +from vpp_neighbor import VppNeighbor + + +class VppInterface(object): + """Generic VPP interface.""" + __metaclass__ = ABCMeta + + @property + def sw_if_index(self): + """Interface index assigned by VPP.""" + return self._sw_if_index + + @property + def remote_mac(self): + """MAC-address of the remote interface "connected" to this interface""" + return self._remote_hosts[0].mac + + @property + def local_mac(self): + """MAC-address of the VPP interface.""" + return self._local_mac + + @property + def local_ip4(self): + """Local IPv4 address on VPP interface (string).""" + return self._local_ip4 + + @property + def local_ip4n(self): + """Local IPv4 address - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET, self._local_ip4) + + @property + def remote_ip4(self): + """IPv4 address of remote peer "connected" to this interface.""" + return self._remote_hosts[0].ip4 + + @property + def remote_ip4n(self): + """IPv4 address of remote peer - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET, self.remote_ip4) + + @property + def local_ip6(self): + """Local IPv6 address on VPP interface (string).""" + return self._local_ip6 + + @property + def local_ip6n(self): + """Local IPv6 address - raw, suitable as API parameter.""" + return socket.inet_pton(socket.AF_INET6, self.local_ip6) + + @property + def local_ip6_ll(self): + """Local IPv6 linnk-local address on VPP interface (string).""" + return self._local_ip6_ll + + @property + def local_ip6n_ll(self): + """Local IPv6 link-local address - raw, suitable as API parameter.""" + return self.local_ip6n_ll + + @property + def remote_ip6(self): + """IPv6 address of remote peer "connected" to this interface.""" + return self._remote_hosts[0].ip6 + + @property + def remote_ip6n(self): + """IPv6 address of remote peer - raw, suitable as API parameter""" + return socket.inet_pton(socket.AF_INET6, self.remote_ip6) + + @property + def name(self): + """Name of the interface.""" + return self._name + + @property + def dump(self): + """RAW result of sw_interface_dump for this interface.""" + return self._dump + + @property + def test(self): + """Test case creating this interface.""" + return self._test + + @property + def remote_hosts(self): + """Remote hosts list""" + return self._remote_hosts + + @remote_hosts.setter + def remote_hosts(self, value): + """ + :param list value: List of remote hosts. + """ + self._remote_hosts = value + self._hosts_by_mac = {} + self._hosts_by_ip4 = {} + self._hosts_by_ip6 = {} + for host in self._remote_hosts: + self._hosts_by_mac[host.mac] = host + self._hosts_by_ip4[host.ip4] = host + self._hosts_by_ip6[host.ip6] = host + + def host_by_mac(self, mac): + """ + :param mac: MAC address to find host by. + :return: Host object assigned to interface. + """ + return self._hosts_by_mac[mac] + + def host_by_ip4(self, ip): + """ + :param ip: IPv4 address to find host by. + :return: Host object assigned to interface. + """ + return self._hosts_by_ip4[ip] + + def host_by_ip6(self, ip): + """ + :param ip: IPv6 address to find host by. + :return: Host object assigned to interface. + """ + return self._hosts_by_ip6[ip] + + def generate_remote_hosts(self, count=1): + """Generate and add remote hosts for the interface. + + :param int count: Number of generated remote hosts. + """ + self._remote_hosts = [] + self._hosts_by_mac = {} + self._hosts_by_ip4 = {} + self._hosts_by_ip6 = {} + for i in range( + 2, count + 2): # 0: network address, 1: local vpp address + mac = "02:%02x:00:00:ff:%02x" % (self.sw_if_index, i) + ip4 = "172.16.%u.%u" % (self.sw_if_index, i) + ip6 = "fd01:%x::%x" % (self.sw_if_index, i) + ip6_ll = mk_ll_addr(mac) + host = Host(mac, ip4, ip6, ip6_ll) + self._remote_hosts.append(host) + self._hosts_by_mac[mac] = host + self._hosts_by_ip4[ip4] = host + self._hosts_by_ip6[ip6] = host + + @abstractmethod + def __init__(self, test): + self._test = test + + self._remote_hosts = [] + self._hosts_by_mac = {} + self._hosts_by_ip4 = {} + self._hosts_by_ip6 = {} + + self.generate_remote_hosts() + + self._local_ip4 = "172.16.%u.1" % self.sw_if_index + self._local_ip4n = socket.inet_pton(socket.AF_INET, self.local_ip4) + self._local_ip4_subnet = "172.16.%u.0" % self.sw_if_index + self._local_ip4n_subnet = socket.inet_pton(socket.AF_INET, + self._local_ip4_subnet) + self._local_ip4_bcast = "172.16.%u.255" % self.sw_if_index + self._local_ip4n_bcast = socket.inet_pton(socket.AF_INET, + self._local_ip4_bcast) + self.local_ip4_prefix_len = 24 + self.has_ip4_config = False + self.ip4_table_id = 0 + + self._local_ip6 = "fd01:%x::1" % self.sw_if_index + self._local_ip6n = socket.inet_pton(socket.AF_INET6, self.local_ip6) + self.local_ip6_prefix_len = 64 + self.has_ip6_config = False + self.ip6_table_id = 0 + + r = self.test.vapi.sw_interface_dump() + for intf in r: + if intf.sw_if_index == self.sw_if_index: + self._name = intf.interface_name.split(b'\0', 1)[0] + self._local_mac = \ + ':'.join(intf.l2_address.encode('hex')[i:i + 2] + for i in range(0, 12, 2)) + self._dump = intf + break + else: + raise Exception( + "Could not find interface with sw_if_index %d " + "in interface dump %s" % + (self.sw_if_index, repr(r))) + self._local_ip6_ll = mk_ll_addr(self.local_mac) + self._local_ip6n_ll = socket.inet_pton(socket.AF_INET6, + self.local_ip6_ll) + + def config_ip4(self): + """Configure IPv4 address on the VPP interface.""" + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, self.local_ip4n, self.local_ip4_prefix_len) + self.has_ip4_config = True + + def unconfig_ip4(self): + """Remove IPv4 address on the VPP interface.""" + try: + if self.has_ip4_config: + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, + self.local_ip4n, + self.local_ip4_prefix_len, + is_add=0) + except AttributeError: + self.has_ip4_config = False + self.has_ip4_config = False + + def configure_ipv4_neighbors(self): + """For every remote host assign neighbor's MAC to IPv4 addresses. + + :param vrf_id: The FIB table / VRF ID. (Default value = 0) + """ + for host in self._remote_hosts: + macn = host.mac.replace(":", "").decode('hex') + ipn = host.ip4n + self.test.vapi.ip_neighbor_add_del( + self.sw_if_index, macn, ipn) + + def config_ip6(self): + """Configure IPv6 address on the VPP interface.""" + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, self._local_ip6n, self.local_ip6_prefix_len, + is_ipv6=1) + self.has_ip6_config = True + + def unconfig_ip6(self): + """Remove IPv6 address on the VPP interface.""" + try: + if self.has_ip6_config: + self.test.vapi.sw_interface_add_del_address( + self.sw_if_index, + self.local_ip6n, + self.local_ip6_prefix_len, + is_ipv6=1, is_add=0) + except AttributeError: + self.has_ip6_config = False + self.has_ip6_config = False + + def configure_ipv6_neighbors(self): + """For every remote host assign neighbor's MAC to IPv6 addresses. + + :param vrf_id: The FIB table / VRF ID. (Default value = 0) + """ + for host in self._remote_hosts: + macn = host.mac.replace(":", "").decode('hex') + ipn = host.ip6n + self.test.vapi.ip_neighbor_add_del( + self.sw_if_index, macn, ipn, is_ipv6=1) + + def unconfig(self): + """Unconfigure IPv6 and IPv4 address on the VPP interface.""" + self.unconfig_ip4() + self.unconfig_ip6() + + def set_table_ip4(self, table_id): + """Set the interface in a IPv4 Table. + + .. note:: Must be called before configuring IP4 addresses. + """ + self.ip4_table_id = table_id + self.test.vapi.sw_interface_set_table( + self.sw_if_index, 0, self.ip4_table_id) + + def set_table_ip6(self, table_id): + """Set the interface in a IPv6 Table. + + .. note:: Must be called before configuring IP6 addresses. + """ + self.ip6_table_id = table_id + self.test.vapi.sw_interface_set_table( + self.sw_if_index, 1, self.ip6_table_id) + + def disable_ipv6_ra(self): + """Configure IPv6 RA suppress on the VPP interface.""" + self.test.vapi.sw_interface_ra_suppress(self.sw_if_index) + + def ip6_ra_config(self, no=0, suppress=0, send_unicast=0): + """Configure IPv6 RA suppress on the VPP interface.""" + self.test.vapi.ip6_sw_interface_ra_config(self.sw_if_index, + no, + suppress, + send_unicast) + + def ip6_ra_prefix(self, address, address_length, is_no=0, + off_link=0, no_autoconfig=0, use_default=0): + """Configure IPv6 RA suppress on the VPP interface.""" + self.test.vapi.ip6_sw_interface_ra_prefix(self.sw_if_index, + address, + address_length, + is_no=is_no, + off_link=off_link, + no_autoconfig=no_autoconfig, + use_default=use_default) + + def admin_up(self): + """Put interface ADMIN-UP.""" + self.test.vapi.sw_interface_set_flags(self.sw_if_index, + admin_up_down=1) + + def admin_down(self): + """Put interface ADMIN-down.""" + self.test.vapi.sw_interface_set_flags(self.sw_if_index, + admin_up_down=0) + + def ip6_enable(self): + """IPv6 Enable interface""" + self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, + enable=1) + + def ip6_disable(self): + """Put interface ADMIN-DOWN.""" + self.test.vapi.ip6_sw_interface_enable_disable(self.sw_if_index, + enable=0) + + def add_sub_if(self, sub_if): + """Register a sub-interface with this interface. + + :param sub_if: sub-interface + """ + if not hasattr(self, 'sub_if'): + self.sub_if = sub_if + else: + if isinstance(self.sub_if, list): + self.sub_if.append(sub_if) + else: + self.sub_if = sub_if + + def enable_mpls(self): + """Enable MPLS on the VPP interface.""" + self.test.vapi.sw_interface_enable_disable_mpls( + self.sw_if_index) + + def disable_mpls(self): + """Enable MPLS on the VPP interface.""" + self.test.vapi.sw_interface_enable_disable_mpls( + self.sw_if_index, 0) + + def is_ip4_entry_in_fib_dump(self, dump): + for i in dump: + if i.address == self.local_ip4n and \ + i.address_length == self.local_ip4_prefix_len and \ + i.table_id == self.ip4_table_id: + return True + return False + + def set_unnumbered(self, ip_sw_if_index): + """ Set the interface to unnumbered via ip_sw_if_index """ + self.test.vapi.sw_interface_set_unnumbered( + self.sw_if_index, + ip_sw_if_index) + + def unset_unnumbered(self, ip_sw_if_index): + """ Unset the interface to unnumbered via ip_sw_if_index """ + self.test.vapi.sw_interface_set_unnumbered( + self.sw_if_index, + ip_sw_if_index, + is_add=0) + + def set_proxy_arp(self, enable=1): + """ Set the interface to enable/disable Proxy ARP """ + self.test.vapi.proxy_arp_intfc_enable_disable( + self.sw_if_index, + enable) diff --git a/test/vpp_ip_route.py b/test/vpp_ip_route.py new file mode 100644 index 00000000..b7993793 --- /dev/null +++ b/test/vpp_ip_route.py @@ -0,0 +1,529 @@ +""" + IP Routes + + object abstractions for representing IP routes in VPP +""" + +from vpp_object import * +from socket import inet_pton, inet_ntop, AF_INET, AF_INET6 + +# from vnet/vnet/mpls/mpls_types.h +MPLS_IETF_MAX_LABEL = 0xfffff +MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 + + +class MRouteItfFlags: + MFIB_ITF_FLAG_NONE = 0 + MFIB_ITF_FLAG_NEGATE_SIGNAL = 1 + MFIB_ITF_FLAG_ACCEPT = 2 + MFIB_ITF_FLAG_FORWARD = 4 + MFIB_ITF_FLAG_SIGNAL_PRESENT = 8 + MFIB_ITF_FLAG_INTERNAL_COPY = 16 + + +class MRouteEntryFlags: + MFIB_ENTRY_FLAG_NONE = 0 + MFIB_ENTRY_FLAG_SIGNAL = 1 + MFIB_ENTRY_FLAG_DROP = 2 + MFIB_ENTRY_FLAG_CONNECTED = 4 + MFIB_ENTRY_FLAG_INHERIT_ACCEPT = 8 + + +class DpoProto: + DPO_PROTO_IP4 = 0 + DPO_PROTO_IP6 = 1 + DPO_PROTO_MPLS = 2 + DPO_PROTO_ETHERNET = 3 + DPO_PROTO_NSH = 4 + + +def find_route(test, ip_addr, len, table_id=0, inet=AF_INET): + if inet == AF_INET: + s = 4 + routes = test.vapi.ip_fib_dump() + else: + s = 16 + routes = test.vapi.ip6_fib_dump() + + route_addr = inet_pton(inet, ip_addr) + for e in routes: + if route_addr == e.address[:s] \ + and len == e.address_length \ + and table_id == e.table_id: + return True + return False + + +class VppIpTable(VppObject): + + def __init__(self, + test, + table_id, + is_ip6=0): + self._test = test + self.table_id = table_id + self.is_ip6 = is_ip6 + + def add_vpp_config(self): + self._test.vapi.ip_table_add_del( + self.table_id, + is_ipv6=self.is_ip6, + is_add=1) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + self._test.vapi.ip_table_add_del( + self.table_id, + is_ipv6=self.is_ip6, + is_add=0) + + def query_vpp_config(self): + # find the default route + return find_route(self._test, + "::" if self.is_ip6 else "0.0.0.0", + 0, + self.table_id, + inet=AF_INET6 if self.is_ip6 == 1 else AF_INET) + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("table-%s-%d" % + ("v6" if self.is_ip6 == 1 else "v4", + self.table_id)) + + +class VppRoutePath(object): + + def __init__( + self, + nh_addr, + nh_sw_if_index, + nh_table_id=0, + labels=[], + nh_via_label=MPLS_LABEL_INVALID, + rpf_id=0, + is_interface_rx=0, + is_resolve_host=0, + is_resolve_attached=0, + proto=DpoProto.DPO_PROTO_IP4): + self.nh_itf = nh_sw_if_index + self.nh_table_id = nh_table_id + self.nh_via_label = nh_via_label + self.nh_labels = labels + self.weight = 1 + self.rpf_id = rpf_id + self.proto = proto + if self.proto is DpoProto.DPO_PROTO_IP6: + self.nh_addr = inet_pton(AF_INET6, nh_addr) + elif self.proto is DpoProto.DPO_PROTO_IP4: + self.nh_addr = inet_pton(AF_INET, nh_addr) + else: + self.nh_addr = inet_pton(AF_INET6, "::") + self.is_resolve_host = is_resolve_host + self.is_resolve_attached = is_resolve_attached + self.is_interface_rx = is_interface_rx + self.is_rpf_id = 0 + if rpf_id != 0: + self.is_rpf_id = 1 + self.nh_itf = rpf_id + + +class VppMRoutePath(VppRoutePath): + + def __init__(self, nh_sw_if_index, flags): + super(VppMRoutePath, self).__init__("0.0.0.0", + nh_sw_if_index) + self.nh_i_flags = flags + + +class VppIpRoute(VppObject): + """ + IP Route + """ + + def __init__(self, test, dest_addr, + dest_addr_len, paths, table_id=0, is_ip6=0, is_local=0, + is_unreach=0, is_prohibit=0): + self._test = test + self.paths = paths + self.dest_addr_len = dest_addr_len + self.table_id = table_id + self.is_ip6 = is_ip6 + self.is_local = is_local + self.is_unreach = is_unreach + self.is_prohibit = is_prohibit + self.dest_addr_p = dest_addr + if is_ip6: + self.dest_addr = inet_pton(AF_INET6, dest_addr) + else: + self.dest_addr = inet_pton(AF_INET, dest_addr) + + def modify(self, paths, is_local=0, + is_unreach=0, is_prohibit=0): + self.paths = paths + self.is_local = is_local + self.is_unreach = is_unreach + self.is_prohibit = is_prohibit + + def add_vpp_config(self): + if self.is_local or self.is_unreach or self.is_prohibit: + self._test.vapi.ip_add_del_route( + self.dest_addr, + self.dest_addr_len, + inet_pton(AF_INET6, "::"), + 0xffffffff, + is_local=self.is_local, + is_unreach=self.is_unreach, + is_prohibit=self.is_prohibit, + table_id=self.table_id, + is_ipv6=self.is_ip6) + else: + for path in self.paths: + self._test.vapi.ip_add_del_route( + self.dest_addr, + self.dest_addr_len, + path.nh_addr, + path.nh_itf, + table_id=self.table_id, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len( + path.nh_labels), + next_hop_via_label=path.nh_via_label, + next_hop_table_id=path.nh_table_id, + is_ipv6=self.is_ip6, + is_resolve_host=path.is_resolve_host, + is_resolve_attached=path.is_resolve_attached, + is_multipath=1 if len(self.paths) > 1 else 0) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + if self.is_local or self.is_unreach or self.is_prohibit: + self._test.vapi.ip_add_del_route( + self.dest_addr, + self.dest_addr_len, + inet_pton(AF_INET6, "::"), + 0xffffffff, + is_local=self.is_local, + is_unreach=self.is_unreach, + is_prohibit=self.is_prohibit, + is_add=0, + table_id=self.table_id, + is_ipv6=self.is_ip6) + else: + for path in self.paths: + self._test.vapi.ip_add_del_route( + self.dest_addr, + self.dest_addr_len, + path.nh_addr, + path.nh_itf, + table_id=self.table_id, + next_hop_table_id=path.nh_table_id, + next_hop_via_label=path.nh_via_label, + is_add=0, + is_ipv6=self.is_ip6) + + def query_vpp_config(self): + return find_route(self._test, + self.dest_addr_p, + self.dest_addr_len, + self.table_id, + inet=AF_INET6 if self.is_ip6 == 1 else AF_INET) + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d:%s/%d" + % (self.table_id, + self.dest_addr_p, + self.dest_addr_len)) + + +class VppIpMRoute(VppObject): + """ + IP Multicast Route + """ + + def __init__(self, test, src_addr, grp_addr, + grp_addr_len, e_flags, paths, table_id=0, + rpf_id=0, is_ip6=0): + self._test = test + self.paths = paths + self.grp_addr_len = grp_addr_len + self.table_id = table_id + self.e_flags = e_flags + self.is_ip6 = is_ip6 + self.rpf_id = rpf_id + + if is_ip6: + self.grp_addr = inet_pton(AF_INET6, grp_addr) + self.src_addr = inet_pton(AF_INET6, src_addr) + else: + self.grp_addr = inet_pton(AF_INET, grp_addr) + self.src_addr = inet_pton(AF_INET, src_addr) + + def add_vpp_config(self): + for path in self.paths: + self._test.vapi.ip_mroute_add_del(self.src_addr, + self.grp_addr, + self.grp_addr_len, + self.e_flags, + path.nh_itf, + path.nh_i_flags, + rpf_id=self.rpf_id, + table_id=self.table_id, + is_ipv6=self.is_ip6) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + for path in self.paths: + self._test.vapi.ip_mroute_add_del(self.src_addr, + self.grp_addr, + self.grp_addr_len, + self.e_flags, + path.nh_itf, + path.nh_i_flags, + table_id=self.table_id, + is_add=0, + is_ipv6=self.is_ip6) + + def update_entry_flags(self, flags): + self.e_flags = flags + self._test.vapi.ip_mroute_add_del(self.src_addr, + self.grp_addr, + self.grp_addr_len, + self.e_flags, + 0xffffffff, + 0, + table_id=self.table_id, + is_ipv6=self.is_ip6) + + def update_rpf_id(self, rpf_id): + self.rpf_id = rpf_id + self._test.vapi.ip_mroute_add_del(self.src_addr, + self.grp_addr, + self.grp_addr_len, + self.e_flags, + 0xffffffff, + 0, + rpf_id=self.rpf_id, + table_id=self.table_id, + is_ipv6=self.is_ip6) + + def update_path_flags(self, itf, flags): + for path in self.paths: + if path.nh_itf == itf: + path.nh_i_flags = flags + break + self._test.vapi.ip_mroute_add_del(self.src_addr, + self.grp_addr, + self.grp_addr_len, + self.e_flags, + path.nh_itf, + path.nh_i_flags, + table_id=self.table_id, + is_ipv6=self.is_ip6) + + def query_vpp_config(self): + dump = self._test.vapi.ip_fib_dump() + for e in dump: + if self.grp_addr == e.address \ + and self.grp_addr_len == e.address_length \ + and self.table_id == e.table_id: + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + if self.is_ip6: + return ("%d:(%s,%s/%d)" + % (self.table_id, + inet_ntop(AF_INET6, self.src_addr), + inet_ntop(AF_INET6, self.grp_addr), + self.grp_addr_len)) + else: + return ("%d:(%s,%s/%d)" + % (self.table_id, + inet_ntop(AF_INET, self.src_addr), + inet_ntop(AF_INET, self.grp_addr), + self.grp_addr_len)) + + +class VppMFibSignal(object): + def __init__(self, test, route, interface, packet): + self.route = route + self.interface = interface + self.packet = packet + self.test = test + + def compare(self, signal): + self.test.assertEqual(self.interface, signal.sw_if_index) + self.test.assertEqual(self.route.table_id, signal.table_id) + self.test.assertEqual(self.route.grp_addr_len, + signal.grp_address_len) + for i in range(self.route.grp_addr_len / 8): + self.test.assertEqual(self.route.grp_addr[i], + signal.grp_address[i]) + if (self.route.grp_addr_len > 32): + for i in range(4): + self.test.assertEqual(self.route.src_addr[i], + signal.src_address[i]) + + +class VppMplsIpBind(VppObject): + """ + MPLS to IP Binding + """ + + def __init__(self, test, local_label, dest_addr, dest_addr_len, + table_id=0, ip_table_id=0, is_ip6=0): + self._test = test + self.dest_addr_len = dest_addr_len + self.dest_addr = dest_addr + self.local_label = local_label + self.table_id = table_id + self.ip_table_id = ip_table_id + self.is_ip6 = is_ip6 + if is_ip6: + self.dest_addrn = inet_pton(AF_INET6, dest_addr) + else: + self.dest_addrn = inet_pton(AF_INET, dest_addr) + + def add_vpp_config(self): + self._test.vapi.mpls_ip_bind_unbind(self.local_label, + self.dest_addrn, + self.dest_addr_len, + table_id=self.table_id, + ip_table_id=self.ip_table_id, + is_ip4=(self.is_ip6 == 0)) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + self._test.vapi.mpls_ip_bind_unbind(self.local_label, + self.dest_addrn, + self.dest_addr_len, + table_id=self.table_id, + ip_table_id=self.ip_table_id, + is_bind=0, + is_ip4=(self.is_ip6 == 0)) + + def query_vpp_config(self): + dump = self._test.vapi.mpls_fib_dump() + for e in dump: + if self.local_label == e.label \ + and self.table_id == e.table_id: + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d:%s binds %d:%s/%d" + % (self.table_id, + self.local_label, + self.ip_table_id, + self.dest_addr, + self.dest_addr_len)) + + +class VppMplsTable(VppObject): + + def __init__(self, + test, + table_id): + self._test = test + self.table_id = table_id + + def add_vpp_config(self): + self._test.vapi.mpls_table_add_del( + self.table_id, + is_add=1) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + self._test.vapi.mpls_table_add_del( + self.table_id, + is_add=0) + + def query_vpp_config(self): + # find the default route + dump = self._test.vapi.mpls_fib_dump() + if len(dump): + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("table-mpls-%d" % (self.table_id)) + + +class VppMplsRoute(VppObject): + """ + MPLS Route/LSP + """ + + def __init__(self, test, local_label, eos_bit, paths, table_id=0, + is_multicast=0): + self._test = test + self.paths = paths + self.local_label = local_label + self.eos_bit = eos_bit + self.table_id = table_id + self.is_multicast = is_multicast + + def add_vpp_config(self): + is_multipath = len(self.paths) > 1 + for path in self.paths: + self._test.vapi.mpls_route_add_del( + self.local_label, + self.eos_bit, + path.proto, + path.nh_addr, + path.nh_itf, + is_multicast=self.is_multicast, + is_multipath=is_multipath, + table_id=self.table_id, + is_interface_rx=path.is_interface_rx, + is_rpf_id=path.is_rpf_id, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len( + path.nh_labels), + next_hop_via_label=path.nh_via_label, + next_hop_table_id=path.nh_table_id) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + for path in self.paths: + self._test.vapi.mpls_route_add_del(self.local_label, + self.eos_bit, + path.proto, + path.nh_addr, + path.nh_itf, + is_rpf_id=path.is_rpf_id, + table_id=self.table_id, + is_add=0) + + def query_vpp_config(self): + dump = self._test.vapi.mpls_fib_dump() + for e in dump: + if self.local_label == e.label \ + and self.eos_bit == e.eos_bit \ + and self.table_id == e.table_id: + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d:%s/%d" + % (self.table_id, + self.local_label, + 20+self.eos_bit)) diff --git a/test/vpp_lo_interface.py b/test/vpp_lo_interface.py new file mode 100644 index 00000000..963123f6 --- /dev/null +++ b/test/vpp_lo_interface.py @@ -0,0 +1,35 @@ +from vpp_object import VppObject +from vpp_interface import VppInterface + + +class VppLoInterface(VppInterface, VppObject): + """VPP loopback interface.""" + + def __init__(self, test, lo_index): + """ Create VPP loopback interface """ + self._test = test + self.add_vpp_config() + super(VppLoInterface, self).__init__(test) + self._lo_index = lo_index + + def add_vpp_config(self): + r = self.test.vapi.create_loopback() + self._sw_if_index = r.sw_if_index + + def remove_vpp_config(self): + self.test.vapi.delete_loopback(self.sw_if_index) + + def query_vpp_config(self): + dump = self.test.vapi.sw_interface_dump() + return self.is_interface_config_in_dump(dump) + + def is_interface_config_in_dump(self, dump): + for i in dump: + if i.interface_name.rstrip(' \t\r\n\0') == self.name and \ + i.sw_if_index == self.sw_if_index: + return True + else: + return False + + def object_id(self): + return "loopback-%d" % self._sw_if_index diff --git a/test/vpp_mpls_tunnel_interface.py b/test/vpp_mpls_tunnel_interface.py new file mode 100644 index 00000000..0542b05c --- /dev/null +++ b/test/vpp_mpls_tunnel_interface.py @@ -0,0 +1,48 @@ + +from vpp_interface import VppInterface +from vpp_ip_route import VppRoutePath +import socket + + +class VppMPLSTunnelInterface(VppInterface): + """ + VPP MPLS Tunnel interface + """ + + def __init__(self, test, paths, is_multicast=0, is_l2=0): + """ Create MPLS Tunnel interface """ + self._sw_if_index = 0 + super(VppMPLSTunnelInterface, self).__init__(test) + self._test = test + self.t_paths = paths + self.is_multicast = is_multicast + self.is_l2 = is_l2 + + def add_vpp_config(self): + self._sw_if_index = 0xffffffff + for path in self.t_paths: + reply = self.test.vapi.mpls_tunnel_add_del( + self._sw_if_index, + 1, # IPv4 next-hop + path.nh_addr, + path.nh_itf, + path.nh_table_id, + path.weight, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len(path.nh_labels), + is_multicast=self.is_multicast, + l2_only=self.is_l2) + self._sw_if_index = reply.sw_if_index + + def remove_vpp_config(self): + for path in self.t_paths: + reply = self.test.vapi.mpls_tunnel_add_del( + self.sw_if_index, + 1, # IPv4 next-hop + path.nh_addr, + path.nh_itf, + path.nh_table_id, + path.weight, + next_hop_out_label_stack=path.nh_labels, + next_hop_n_out_labels=len(path.nh_labels), + is_add=0) diff --git a/test/vpp_neighbor.py b/test/vpp_neighbor.py new file mode 100644 index 00000000..5919cf8e --- /dev/null +++ b/test/vpp_neighbor.py @@ -0,0 +1,79 @@ +""" + Neighbour Entries + + object abstractions for ARP and ND +""" + +from socket import inet_pton, inet_ntop, AF_INET, AF_INET6 +from vpp_object import * +from util import mactobinary + + +def find_nbr(test, sw_if_index, ip_addr, is_static=0, inet=AF_INET): + nbrs = test.vapi.ip_neighbor_dump(sw_if_index, + is_ipv6=1 if AF_INET6 == inet else 0) + if inet == AF_INET: + s = 4 + else: + s = 16 + nbr_addr = inet_pton(inet, ip_addr) + + for n in nbrs: + if nbr_addr == n.ip_address[:s] \ + and is_static == n.is_static: + return True + return False + + +class VppNeighbor(VppObject): + """ + ARP Entry + """ + + def __init__(self, test, sw_if_index, mac_addr, nbr_addr, + af=AF_INET, is_static=False, is_no_fib_entry=0): + self._test = test + self.sw_if_index = sw_if_index + self.mac_addr = mactobinary(mac_addr) + self.af = af + self.is_static = is_static + self.is_no_fib_entry = is_no_fib_entry + self.nbr_addr = inet_pton(af, nbr_addr) + + def add_vpp_config(self): + self._test.vapi.ip_neighbor_add_del( + self.sw_if_index, + self.mac_addr, + self.nbr_addr, + is_add=1, + is_ipv6=1 if AF_INET6 == self.af else 0, + is_static=self.is_static, + is_no_adj_fib=self.is_no_fib_entry) + self._test.registry.register(self, self._test.logger) + + def remove_vpp_config(self): + self._test.vapi.ip_neighbor_add_del( + self.sw_if_index, + self.mac_addr, + self.nbr_addr, + is_ipv6=1 if AF_INET6 == self.af else 0, + is_add=0, + is_static=self.is_static) + + def query_vpp_config(self): + dump = self._test.vapi.ip_neighbor_dump( + self.sw_if_index, + is_ipv6=1 if AF_INET6 == self.af else 0) + for n in dump: + if self.nbr_addr == n.ip_address \ + and self.is_static == n.is_static: + return True + return False + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d:%s" + % (self.sw_if_index, + inet_ntop(self.af, self.nbr_addr))) diff --git a/test/vpp_object.py b/test/vpp_object.py new file mode 100644 index 00000000..a1cf42fc --- /dev/null +++ b/test/vpp_object.py @@ -0,0 +1,87 @@ +""" abstract vpp object and object registry """ + +from abc import ABCMeta, abstractmethod + + +class VppObject(object): + """ Abstract vpp object """ + __metaclass__ = ABCMeta + + @abstractmethod + def add_vpp_config(self): + """ Add the configuration for this object to vpp. """ + pass + + @abstractmethod + def query_vpp_config(self): + """Query the vpp configuration. + + :return: True if the object is configured""" + pass + + @abstractmethod + def remove_vpp_config(self): + """ Remove the configuration for this object from vpp. """ + pass + + @abstractmethod + def object_id(self): + """ Return a unique string representing this object. """ + pass + + +class VppObjectRegistry(object): + """ Class which handles automatic configuration cleanup. """ + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + if not hasattr(self, "_object_registry"): + self._object_registry = [] + if not hasattr(self, "_object_dict"): + self._object_dict = dict() + + def register(self, obj, logger): + """ Register an object in the registry. """ + if obj.object_id() not in self._object_dict: + self._object_registry.append(obj) + self._object_dict[obj.object_id()] = obj + logger.debug("REG: registering %s" % obj) + else: + logger.debug("REG: duplicate add, ignoring (%s)" % obj) + + def unregister_all(self, logger): + """ Remove all object registrations from registry. """ + logger.debug("REG: removing all object registrations") + self._object_registry = [] + self._object_dict = dict() + + def remove_vpp_config(self, logger): + """ + Remove configuration (if present) from vpp and then remove all objects + from the registry. + """ + if not self._object_registry: + logger.info("REG: No objects registered for auto-cleanup.") + return + logger.info("REG: Removing VPP configuration for registered objects") + # remove the config in reverse order as there might be dependencies + for obj in reversed(self._object_registry): + if obj.query_vpp_config(): + logger.info("REG: Removing configuration for %s" % obj) + obj.remove_vpp_config() + else: + logger.info( + "REG: Skipping removal for %s, configuration not present" % + obj) + failed = [] + for obj in self._object_registry: + if obj.query_vpp_config(): + failed.append(obj) + self.unregister_all(logger) + if failed: + logger.error("REG: Couldn't remove configuration for object(s):") + for obj in failed: + logger.error(repr(obj)) + raise Exception("Couldn't remove configuration for object(s): %s" % + (", ".join(str(x) for x in failed))) diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py new file mode 100644 index 00000000..2eab6c6e --- /dev/null +++ b/test/vpp_papi_provider.py @@ -0,0 +1,2390 @@ +import os +import fnmatch +import time +from hook import Hook +from collections import deque + +# Sphinx creates auto-generated documentation by importing the python source +# files and collecting the docstrings from them. The NO_VPP_PAPI flag allows +# the vpp_papi_provider.py file to be importable without having to build +# the whole vpp api if the user only wishes to generate the test documentation. +do_import = True +try: + no_vpp_papi = os.getenv("NO_VPP_PAPI") + if no_vpp_papi == "1": + do_import = False +except: + pass + +if do_import: + from vpp_papi import VPP + +# from vnet/vnet/mpls/mpls_types.h +MPLS_IETF_MAX_LABEL = 0xfffff +MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 + + +class L2_VTR_OP: + L2_DISABLED = 0 + L2_PUSH_1 = 1 + L2_PUSH_2 = 2 + L2_POP_1 = 3 + L2_POP_2 = 4 + L2_TRANSLATE_1_1 = 5 + L2_TRANSLATE_1_2 = 6 + L2_TRANSLATE_2_1 = 7 + L2_TRANSLATE_2_2 = 8 + + +class UnexpectedApiReturnValueError(Exception): + """ exception raised when the API return value is unexpected """ + pass + + +class VppPapiProvider(object): + """VPP-api provider using vpp-papi + + @property hook: hook object providing before and after api/cli hooks + """ + + _zero, _negative = range(2) + + def __init__(self, name, shm_prefix, test_class): + self.hook = Hook("vpp-papi-provider") + self.name = name + self.shm_prefix = shm_prefix + self.test_class = test_class + self._expect_api_retval = self._zero + self._expect_stack = [] + jsonfiles = [] + + install_dir = os.getenv('VPP_TEST_INSTALL_PATH') + for root, dirnames, filenames in os.walk(install_dir): + for filename in fnmatch.filter(filenames, '*.api.json'): + jsonfiles.append(os.path.join(root, filename)) + + self.vpp = VPP(jsonfiles, logger=test_class.logger) + self._events = deque() + + def __enter__(self): + return self + + def expect_negative_api_retval(self): + """ Expect API failure """ + self._expect_stack.append(self._expect_api_retval) + self._expect_api_retval = self._negative + return self + + def expect_zero_api_retval(self): + """ Expect API success """ + self._expect_stack.append(self._expect_api_retval) + self._expect_api_retval = self._zero + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._expect_api_retval = self._expect_stack.pop() + + def register_hook(self, hook): + """Replace hook registration with new hook + + :param hook: + + """ + self.hook = hook + + def collect_events(self): + """ Collect all events from the internal queue and clear the queue. """ + e = self._events + self._events = deque() + return e + + def wait_for_event(self, timeout, name=None): + """ Wait for and return next event. """ + if name: + self.test_class.logger.debug("Expecting event '%s' within %ss", + name, timeout) + else: + self.test_class.logger.debug("Expecting event within %ss", + timeout) + if self._events: + self.test_class.logger.debug("Not waiting, event already queued") + limit = time.time() + timeout + while time.time() < limit: + if self._events: + e = self._events.popleft() + if name and type(e).__name__ != name: + raise Exception( + "Unexpected event received: %s, expected: %s" % + (type(e).__name__, name)) + self.test_class.logger.debug("Returning event %s:%s" % + (name, e)) + return e + time.sleep(0) # yield + raise Exception("Event did not occur within timeout") + + def __call__(self, name, event): + """ Enqueue event in the internal event queue. """ + # FIXME use the name instead of relying on type(e).__name__ ? + # FIXME #2 if this throws, it is eaten silently, Ole? + self.test_class.logger.debug("New event: %s: %s" % (name, event)) + self._events.append(event) + + def connect(self): + """Connect the API to VPP""" + self.vpp.connect(self.name, self.shm_prefix) + self.papi = self.vpp.api + self.vpp.register_event_callback(self) + + def disconnect(self): + """Disconnect the API from VPP""" + self.vpp.disconnect() + + def api(self, api_fn, api_args, expected_retval=0): + """ Call API function and check it's return value. + Call the appropriate hooks before and after the API call + + :param api_fn: API function to call + :param api_args: tuple of API function arguments + :param expected_retval: Expected return value (Default value = 0) + :returns: reply from the API + + """ + self.hook.before_api(api_fn.__name__, api_args) + reply = api_fn(**api_args) + if self._expect_api_retval == self._negative: + if hasattr(reply, 'retval') and reply.retval >= 0: + msg = "API call passed unexpectedly: expected negative "\ + "return value instead of %d in %s" % \ + (reply.retval, repr(reply)) + self.test_class.logger.info(msg) + raise UnexpectedApiReturnValueError(msg) + elif self._expect_api_retval == self._zero: + if hasattr(reply, 'retval') and reply.retval != expected_retval: + msg = "API call failed, expected %d return value instead "\ + "of %d in %s" % (expected_retval, reply.retval, + repr(reply)) + self.test_class.logger.info(msg) + raise UnexpectedApiReturnValueError(msg) + else: + raise Exception("Internal error, unexpected value for " + "self._expect_api_retval %s" % + self._expect_api_retval) + self.hook.after_api(api_fn.__name__, api_args) + return reply + + def cli(self, cli): + """ Execute a CLI, calling the before/after hooks appropriately. + + :param cli: CLI to execute + :returns: CLI output + + """ + self.hook.before_cli(cli) + cli += '\n' + r = self.papi.cli_inband(length=len(cli), cmd=cli) + self.hook.after_cli(cli) + if hasattr(r, 'reply'): + return r.reply.decode().rstrip('\x00') + + def ppcli(self, cli): + """ Helper method to print CLI command in case of info logging level. + + :param cli: CLI to execute + :returns: CLI output + """ + return cli + "\n" + str(self.cli(cli)) + + def _convert_mac(self, mac): + return int(mac.replace(":", ""), 16) << 16 + + def show_version(self): + """ """ + return self.api(self.papi.show_version, {}) + + def pg_create_interface(self, pg_index): + """ + + :param pg_index: + + """ + return self.api(self.papi.pg_create_interface, + {"interface_id": pg_index}) + + def sw_interface_dump(self, filter=None): + """ + + :param filter: (Default value = None) + + """ + if filter is not None: + args = {"name_filter_valid": 1, "name_filter": filter} + else: + args = {} + return self.api(self.papi.sw_interface_dump, args) + + def sw_interface_set_table(self, sw_if_index, is_ipv6, table_id): + """ Set the IPvX Table-id for the Interface + + :param sw_if_index: + :param is_ipv6: + :param table_id: + + """ + return self.api(self.papi.sw_interface_set_table, + {'sw_if_index': sw_if_index, 'is_ipv6': is_ipv6, + 'vrf_id': table_id}) + + def sw_interface_add_del_address(self, sw_if_index, addr, addr_len, + is_ipv6=0, is_add=1, del_all=0): + """ + + :param addr: param is_ipv6: (Default value = 0) + :param sw_if_index: + :param addr_len: + :param is_ipv6: (Default value = 0) + :param is_add: (Default value = 1) + :param del_all: (Default value = 0) + + """ + return self.api(self.papi.sw_interface_add_del_address, + {'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'del_all': del_all, + 'address_length': addr_len, + 'address': addr}) + + def sw_interface_set_unnumbered(self, sw_if_index, ip_sw_if_index, + is_add=1): + """ Set the Interface to be unnumbered + + :param is_add: (Default value = 1) + :param sw_if_index - interface That will be unnumbered + :param ip_sw_if_index - interface with an IP addres + + """ + return self.api(self.papi.sw_interface_set_unnumbered, + {'sw_if_index': ip_sw_if_index, + 'unnumbered_sw_if_index': sw_if_index, + 'is_add': is_add}) + + def sw_interface_enable_disable_mpls(self, sw_if_index, + is_enable=1): + """ + Enable/Disable MPLS on the interface + :param sw_if_index: + :param is_enable: (Default value = 1) + + """ + return self.api(self.papi.sw_interface_set_mpls_enable, + {'sw_if_index': sw_if_index, + 'enable': is_enable}) + + def sw_interface_ra_suppress(self, sw_if_index, suppress=1): + return self.api(self.papi.sw_interface_ip6nd_ra_config, + {'sw_if_index': sw_if_index, + 'suppress': suppress}) + + def set_ip_flow_hash(self, + table_id, + src=1, + dst=1, + sport=1, + dport=1, + proto=1, + reverse=0, + is_ip6=0): + return self.api(self.papi.set_ip_flow_hash, + {'vrf_id': table_id, + 'src': src, + 'dst': dst, + 'dport': dport, + 'sport': sport, + 'proto': proto, + 'reverse': reverse, + 'is_ipv6': is_ip6}) + + def ip6_nd_proxy(self, address, sw_if_index, is_del=0): + return self.api(self.papi.ip6nd_proxy_add_del, + {'address': address, + 'sw_if_index': sw_if_index, + 'is_del': is_del}) + + def ip6_sw_interface_ra_config(self, sw_if_index, + no, + suppress, + send_unicast): + return self.api(self.papi.sw_interface_ip6nd_ra_config, + {'sw_if_index': sw_if_index, + 'is_no': no, + 'suppress': suppress, + 'send_unicast': send_unicast}) + + def ip6_sw_interface_ra_prefix(self, + sw_if_index, + address, + address_length, + use_default=0, + no_advertise=0, + off_link=0, + no_autoconfig=0, + no_onlink=0, + is_no=0, + val_lifetime=0xffffffff, + pref_lifetime=0xffffffff): + return self.api(self.papi.sw_interface_ip6nd_ra_prefix, + {'sw_if_index': sw_if_index, + 'address': address, + 'address_length': address_length, + 'use_default': use_default, + 'no_advertise': no_advertise, + 'off_link': off_link, + 'no_autoconfig': no_autoconfig, + 'no_onlink': no_onlink, + 'is_no': is_no, + 'val_lifetime': val_lifetime, + 'pref_lifetime': pref_lifetime}) + + def ip6_sw_interface_enable_disable(self, sw_if_index, enable): + """ + Enable/Disable An interface for IPv6 + """ + return self.api(self.papi.sw_interface_ip6_enable_disable, + {'sw_if_index': sw_if_index, + 'enable': enable}) + + def vxlan_add_del_tunnel( + self, + src_addr, + dst_addr, + mcast_sw_if_index=0xFFFFFFFF, + is_add=1, + is_ipv6=0, + encap_vrf_id=0, + decap_next_index=0xFFFFFFFF, + vni=0): + """ + + :param dst_addr: + :param src_addr: + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param encap_vrf_id: (Default value = 0) + :param decap_next_index: (Default value = 0xFFFFFFFF) + :param mcast_sw_if_index: (Default value = 0xFFFFFFFF) + :param vni: (Default value = 0) + + """ + return self.api(self.papi.vxlan_add_del_tunnel, + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'src_address': src_addr, + 'dst_address': dst_addr, + 'mcast_sw_if_index': mcast_sw_if_index, + 'encap_vrf_id': encap_vrf_id, + 'decap_next_index': decap_next_index, + 'vni': vni}) + + def bridge_domain_add_del(self, bd_id, flood=1, uu_flood=1, forward=1, + learn=1, arp_term=0, is_add=1): + """Create/delete bridge domain. + + :param int bd_id: Bridge domain index. + :param int flood: Enable/disable bcast/mcast flooding in the BD. + (Default value = 1) + :param int uu_flood: Enable/disable unknown unicast flood in the BD. + (Default value = 1) + :param int forward: Enable/disable forwarding on all interfaces in + the BD. (Default value = 1) + :param int learn: Enable/disable learning on all interfaces in the BD. + (Default value = 1) + :param int arp_term: Enable/disable arp termination in the BD. + (Default value = 1) + :param int is_add: Add or delete flag. (Default value = 1) + """ + return self.api(self.papi.bridge_domain_add_del, + {'bd_id': bd_id, + 'flood': flood, + 'uu_flood': uu_flood, + 'forward': forward, + 'learn': learn, + 'arp_term': arp_term, + 'is_add': is_add}) + + def bd_ip_mac_add_del(self, bd_id, mac, ip, is_ipv6=0, is_add=1): + return self.api(self.papi.bd_ip_mac_add_del, + {'bd_id': bd_id, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'ip_address': ip, + 'mac_address': mac}) + + def want_ip4_arp_events(self, enable_disable=1, address=0): + return self.api(self.papi.want_ip4_arp_events, + {'enable_disable': enable_disable, + 'address': address, + 'pid': os.getpid(), }) + + def want_ip6_nd_events(self, enable_disable=1, address=0): + return self.api(self.papi.want_ip6_nd_events, + {'enable_disable': enable_disable, + 'address': address, + 'pid': os.getpid(), }) + + def want_macs_learn_events(self, enable_disable=1, scan_delay=0, + max_macs_in_event=0, learn_limit=0): + return self.api(self.papi.want_l2_macs_events, + {'enable_disable': enable_disable, + 'scan_delay': scan_delay, + 'max_macs_in_event': max_macs_in_event, + 'learn_limit': learn_limit, + 'pid': os.getpid(), }) + + def l2fib_add_del(self, mac, bd_id, sw_if_index, is_add=1, static_mac=0, + filter_mac=0, bvi_mac=0): + """Create/delete L2 FIB entry. + + :param str mac: MAC address to create FIB entry for. + :param int bd_id: Bridge domain index. + :param int sw_if_index: Software interface index of the interface. + :param int is_add: Add or delete flag. (Default value = 1) + :param int static_mac: Set to 1 to create static MAC entry. + (Default value = 0) + :param int filter_mac: Set to 1 to drop packet that's source or + destination MAC address contains defined MAC address. + (Default value = 0) + :param int bvi_mac: Set to 1 to create entry that points to BVI + interface. (Default value = 0) + """ + return self.api(self.papi.l2fib_add_del, + {'mac': self._convert_mac(mac), + 'bd_id': bd_id, + 'sw_if_index': sw_if_index, + 'is_add': is_add, + 'static_mac': static_mac, + 'filter_mac': filter_mac, + 'bvi_mac': bvi_mac}) + + def l2fib_flush_int(self, sw_if_index): + """Flush L2 FIB entries for sw_if_index. + + :param int sw_if_index: Software interface index of the interface. + """ + return self.api(self.papi.l2fib_flush_int, + {'sw_if_index': sw_if_index}) + + def l2fib_flush_bd(self, bd_id): + """Flush L2 FIB entries for bd_id. + + :param int sw_if_index: Bridge Domain id. + """ + return self.api(self.papi.l2fib_flush_bd, + {'bd_id': bd_id}) + + def l2fib_flush_all(self): + """Flush all L2 FIB. + """ + return self.api(self.papi.l2fib_flush_all, {}) + + def sw_interface_set_l2_bridge(self, sw_if_index, bd_id, + shg=0, bvi=0, enable=1): + """Add/remove interface to/from bridge domain. + + :param int sw_if_index: Software interface index of the interface. + :param int bd_id: Bridge domain index. + :param int shg: Split-horizon group index. (Default value = 0) + :param int bvi: Set interface as a bridge group virtual interface. + (Default value = 0) + :param int enable: Add or remove interface. (Default value = 1) + """ + return self.api(self.papi.sw_interface_set_l2_bridge, + {'rx_sw_if_index': sw_if_index, + 'bd_id': bd_id, + 'shg': shg, + 'bvi': bvi, + 'enable': enable}) + + def bridge_flags(self, bd_id, is_set, feature_bitmap): + """Enable/disable required feature of the bridge domain with defined ID. + + :param int bd_id: Bridge domain ID. + :param int is_set: Set to 1 to enable, set to 0 to disable the feature. + :param int feature_bitmap: Bitmap value of the feature to be set: + - learn (1 << 0), + - forward (1 << 1), + - flood (1 << 2), + - uu-flood (1 << 3) or + - arp-term (1 << 4). + """ + return self.api(self.papi.bridge_flags, + {'bd_id': bd_id, + 'is_set': is_set, + 'feature_bitmap': feature_bitmap}) + + def bridge_domain_dump(self, bd_id=0): + """ + + :param int bd_id: Bridge domain ID. (Default value = 0 => dump of all + existing bridge domains returned) + :return: Dictionary of bridge domain(s) data. + """ + return self.api(self.papi.bridge_domain_dump, + {'bd_id': bd_id}) + + def sw_interface_set_l2_xconnect(self, rx_sw_if_index, tx_sw_if_index, + enable): + """Create or delete unidirectional cross-connect from Tx interface to + Rx interface. + + :param int rx_sw_if_index: Software interface index of Rx interface. + :param int tx_sw_if_index: Software interface index of Tx interface. + :param int enable: Create cross-connect if equal to 1, delete + cross-connect if equal to 0. + + """ + return self.api(self.papi.sw_interface_set_l2_xconnect, + {'rx_sw_if_index': rx_sw_if_index, + 'tx_sw_if_index': tx_sw_if_index, + 'enable': enable}) + + def sw_interface_set_l2_tag_rewrite( + self, + sw_if_index, + vtr_oper, + push=0, + tag1=0, + tag2=0): + """L2 interface vlan tag rewrite configure request + :param client_index - opaque cookie to identify the sender + :param context - sender context, to match reply w/ request + :param sw_if_index - interface the operation is applied to + :param vtr_op - Choose from l2_vtr_op_t enum values + :param push_dot1q - first pushed flag dot1q id set, else dot1ad + :param tag1 - Needed for any push or translate vtr op + :param tag2 - Needed for any push 2 or translate x-2 vtr ops + + """ + return self.api(self.papi.l2_interface_vlan_tag_rewrite, + {'sw_if_index': sw_if_index, + 'vtr_op': vtr_oper, + 'push_dot1q': push, + 'tag1': tag1, + 'tag2': tag2}) + + def sw_interface_set_flags(self, sw_if_index, admin_up_down): + """ + + :param admin_up_down: + :param sw_if_index: + + """ + return self.api(self.papi.sw_interface_set_flags, + {'sw_if_index': sw_if_index, + 'admin_up_down': admin_up_down}) + + def create_subif(self, sw_if_index, sub_id, outer_vlan, inner_vlan, + no_tags=0, one_tag=0, two_tags=0, dot1ad=0, exact_match=0, + default_sub=0, outer_vlan_id_any=0, inner_vlan_id_any=0): + """Create subinterface + from vpe.api: set dot1ad = 0 for dot1q, set dot1ad = 1 for dot1ad + + :param sub_id: param inner_vlan: + :param sw_if_index: + :param outer_vlan: + :param inner_vlan: + :param no_tags: (Default value = 0) + :param one_tag: (Default value = 0) + :param two_tags: (Default value = 0) + :param dot1ad: (Default value = 0) + :param exact_match: (Default value = 0) + :param default_sub: (Default value = 0) + :param outer_vlan_id_any: (Default value = 0) + :param inner_vlan_id_any: (Default value = 0) + + """ + return self.api( + self.papi.create_subif, + {'sw_if_index': sw_if_index, + 'sub_id': sub_id, + 'no_tags': no_tags, + 'one_tag': one_tag, + 'two_tags': two_tags, + 'dot1ad': dot1ad, + 'exact_match': exact_match, + 'default_sub': default_sub, + 'outer_vlan_id_any': outer_vlan_id_any, + 'inner_vlan_id_any': inner_vlan_id_any, + 'outer_vlan_id': outer_vlan, + 'inner_vlan_id': inner_vlan}) + + def create_p2pethernet_subif(self, sw_if_index, remote_mac, subif_id): + """Create p2p ethernet subinterface + + :param sw_if_index: main (parent) interface + :param remote_mac: client (remote) mac address + + """ + return self.api( + self.papi.p2p_ethernet_add, + {'parent_if_index': sw_if_index, + 'remote_mac': remote_mac, + 'subif_id': subif_id}) + + def delete_subif(self, sw_if_index): + """Delete subinterface + + :param sw_if_index: + """ + return self.api(self.papi.delete_subif, + {'sw_if_index': sw_if_index}) + + def delete_p2pethernet_subif(self, sw_if_index, remote_mac): + """Delete p2p ethernet subinterface + + :param sw_if_index: main (parent) interface + :param remote_mac: client (remote) mac address + + """ + return self.api( + self.papi.p2p_ethernet_del, + {'parent_if_index': sw_if_index, + 'remote_mac': remote_mac}) + + def create_vlan_subif(self, sw_if_index, vlan): + """ + + :param vlan: + :param sw_if_index: + + """ + return self.api(self.papi.create_vlan_subif, + {'sw_if_index': sw_if_index, + 'vlan_id': vlan}) + + def create_loopback(self, mac=''): + """ + + :param mac: (Optional) + """ + return self.api(self.papi.create_loopback, + {'mac_address': mac}) + + def delete_loopback(self, sw_if_index): + return self.api(self.papi.delete_loopback, + {'sw_if_index': sw_if_index, }) + + def ip_table_add_del(self, + table_id, + is_add=1, + is_ipv6=0): + """ + + :param table_id + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + + """ + + return self.api( + self.papi.ip_table_add_del, + {'table_id': table_id, + 'is_add': is_add, + 'is_ipv6': is_ipv6}) + + def ip_add_del_route( + self, + dst_address, + dst_address_length, + next_hop_address, + next_hop_sw_if_index=0xFFFFFFFF, + table_id=0, + next_hop_table_id=0, + next_hop_weight=1, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, + is_resolve_host=0, + is_resolve_attached=0, + classify_table_index=0xFFFFFFFF, + is_add=1, + is_drop=0, + is_unreach=0, + is_prohibit=0, + is_ipv6=0, + is_local=0, + is_classify=0, + is_multipath=0, + not_last=0): + """ + + :param dst_address_length: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param dst_address: + :param next_hop_address: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param vrf_id: (Default value = 0) + :param lookup_in_vrf: (Default value = 0) + :param classify_table_index: (Default value = 0xFFFFFFFF) + :param is_add: (Default value = 1) + :param is_drop: (Default value = 0) + :param is_ipv6: (Default value = 0) + :param is_local: (Default value = 0) + :param is_classify: (Default value = 0) + :param is_multipath: (Default value = 0) + :param is_resolve_host: (Default value = 0) + :param is_resolve_attached: (Default value = 0) + :param not_last: (Default value = 0) + :param next_hop_weight: (Default value = 1) + + """ + + return self.api( + self.papi.ip_add_del_route, + {'next_hop_sw_if_index': next_hop_sw_if_index, + 'table_id': table_id, + 'classify_table_index': classify_table_index, + 'next_hop_table_id': next_hop_table_id, + 'is_add': is_add, + 'is_drop': is_drop, + 'is_unreach': is_unreach, + 'is_prohibit': is_prohibit, + 'is_ipv6': is_ipv6, + 'is_local': is_local, + 'is_classify': is_classify, + 'is_multipath': is_multipath, + 'is_resolve_host': is_resolve_host, + 'is_resolve_attached': is_resolve_attached, + 'not_last': not_last, + 'next_hop_weight': next_hop_weight, + 'dst_address_length': dst_address_length, + 'dst_address': dst_address, + 'next_hop_address': next_hop_address, + 'next_hop_n_out_labels': next_hop_n_out_labels, + 'next_hop_via_label': next_hop_via_label, + 'next_hop_out_label_stack': next_hop_out_label_stack}) + + def ip_fib_dump(self): + return self.api(self.papi.ip_fib_dump, {}) + + def ip6_fib_dump(self): + return self.api(self.papi.ip6_fib_dump, {}) + + def ip_neighbor_add_del(self, + sw_if_index, + mac_address, + dst_address, + is_add=1, + is_ipv6=0, + is_static=0, + is_no_adj_fib=0, + ): + """ Add neighbor MAC to IPv4 or IPv6 address. + + :param sw_if_index: + :param mac_address: + :param dst_address: + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param is_static: (Default value = 0) + :param is_no_adj_fib: (Default value = 0) + """ + + return self.api( + self.papi.ip_neighbor_add_del, + {'sw_if_index': sw_if_index, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'is_static': is_static, + 'is_no_adj_fib': is_no_adj_fib, + 'mac_address': mac_address, + 'dst_address': dst_address + } + ) + + def ip_neighbor_dump(self, + sw_if_index, + is_ipv6=0): + """ Return IP neighbor dump. + + :param sw_if_index: + :param int is_ipv6: 1 for IPv6 neighbor, 0 for IPv4. (Default = 0) + """ + + return self.api( + self.papi.ip_neighbor_dump, + {'is_ipv6': is_ipv6, + 'sw_if_index': sw_if_index + } + ) + + def proxy_arp_add_del(self, + low_address, + hi_address, + vrf_id=0, + is_add=1): + """ Config Proxy Arp Range. + + :param low_address: Start address in the rnage to Proxy for + :param hi_address: End address in the rnage to Proxy for + :param vrf_id: The VRF/table in which to proxy + """ + + return self.api( + self.papi.proxy_arp_add_del, + {'vrf_id': vrf_id, + 'is_add': is_add, + 'low_address': low_address, + 'hi_address': hi_address, + } + ) + + def proxy_arp_intfc_enable_disable(self, + sw_if_index, + is_enable=1): + """ Enable/Disable an interface for proxy ARP requests + + :param sw_if_index: Interface + :param enable_disable: Enable/Disable + """ + + return self.api( + self.papi.proxy_arp_intfc_enable_disable, + {'sw_if_index': sw_if_index, + 'enable_disable': is_enable + } + ) + + def reset_vrf(self, + vrf_id, + is_ipv6=0, + ): + """ Reset VRF (remove all routes etc.) request. + + :param int vrf_id: ID of the FIB table / VRF to reset. + :param int is_ipv6: 1 for IPv6 neighbor, 0 for IPv4. (Default = 0) + """ + + return self.api( + self.papi.reset_vrf, + {'vrf_id': vrf_id, + 'is_ipv6': is_ipv6, + } + ) + + def reset_fib(self, + vrf_id, + is_ipv6=0, + ): + """ Reset VRF (remove all routes etc.) request. + + :param int vrf_id: ID of the FIB table / VRF to reset. + :param int is_ipv6: 1 for IPv6 neighbor, 0 for IPv4. (Default = 0) + """ + + return self.api( + self.papi.reset_fib, + {'vrf_id': vrf_id, + 'is_ipv6': is_ipv6, + } + ) + + def ip_dump(self, + is_ipv6=0, + ): + """ Return IP dump. + + :param int is_ipv6: 1 for IPv6 neighbor, 0 for IPv4. (Default = 0) + """ + + return self.api( + self.papi.ip_dump, + {'is_ipv6': is_ipv6, + } + ) + + def sw_interface_span_enable_disable( + self, sw_if_index_from, sw_if_index_to, state=1, is_l2=0): + """ + + :param sw_if_index_from: + :param sw_if_index_to: + :param state: + :param is_l2: + """ + return self.api(self.papi.sw_interface_span_enable_disable, + {'sw_if_index_from': sw_if_index_from, + 'sw_if_index_to': sw_if_index_to, + 'state': state, + 'is_l2': is_l2, + }) + + def gre_tunnel_add_del(self, + src_address, + dst_address, + outer_fib_id=0, + is_teb=0, + is_add=1, + is_ip6=0): + """ Add a GRE tunnel + + :param src_address: + :param dst_address: + :param outer_fib_id: (Default value = 0) + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param is_teb: (Default value = 0) + """ + + return self.api( + self.papi.gre_add_del_tunnel, + {'is_add': is_add, + 'is_ipv6': is_ip6, + 'teb': is_teb, + 'src_address': src_address, + 'dst_address': dst_address, + 'outer_fib_id': outer_fib_id} + ) + + def mpls_fib_dump(self): + return self.api(self.papi.mpls_fib_dump, {}) + + def mpls_table_add_del( + self, + table_id, + is_add=1): + """ + + :param table_id + :param is_add: (Default value = 1) + + """ + + return self.api( + self.papi.mpls_table_add_del, + {'mt_table_id': table_id, + 'mt_is_add': is_add}) + + def mpls_route_add_del( + self, + label, + eos, + next_hop_proto, + next_hop_address, + next_hop_sw_if_index=0xFFFFFFFF, + table_id=0, + next_hop_table_id=0, + next_hop_weight=1, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, + is_resolve_host=0, + is_resolve_attached=0, + is_interface_rx=0, + is_rpf_id=0, + is_multicast=0, + is_add=1, + is_drop=0, + is_multipath=0, + classify_table_index=0xFFFFFFFF, + is_classify=0, + not_last=0): + """ + + :param dst_address_length: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param dst_address: + :param next_hop_address: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param vrf_id: (Default value = 0) + :param lookup_in_vrf: (Default value = 0) + :param classify_table_index: (Default value = 0xFFFFFFFF) + :param is_add: (Default value = 1) + :param is_drop: (Default value = 0) + :param is_ipv6: (Default value = 0) + :param is_local: (Default value = 0) + :param is_classify: (Default value = 0) + :param is_multipath: (Default value = 0) + :param is_multicast: (Default value = 0) + :param is_resolve_host: (Default value = 0) + :param is_resolve_attached: (Default value = 0) + :param not_last: (Default value = 0) + :param next_hop_weight: (Default value = 1) + + """ + + return self.api( + self.papi.mpls_route_add_del, + {'mr_label': label, + 'mr_eos': eos, + 'mr_table_id': table_id, + 'mr_classify_table_index': classify_table_index, + 'mr_is_add': is_add, + 'mr_is_classify': is_classify, + 'mr_is_multipath': is_multipath, + 'mr_is_multicast': is_multicast, + 'mr_is_resolve_host': is_resolve_host, + 'mr_is_resolve_attached': is_resolve_attached, + 'mr_is_interface_rx': is_interface_rx, + 'mr_is_rpf_id': is_rpf_id, + 'mr_next_hop_proto': next_hop_proto, + 'mr_next_hop_weight': next_hop_weight, + 'mr_next_hop': next_hop_address, + 'mr_next_hop_n_out_labels': next_hop_n_out_labels, + 'mr_next_hop_sw_if_index': next_hop_sw_if_index, + 'mr_next_hop_table_id': next_hop_table_id, + 'mr_next_hop_via_label': next_hop_via_label, + 'mr_next_hop_out_label_stack': next_hop_out_label_stack}) + + def mpls_ip_bind_unbind( + self, + label, + dst_address, + dst_address_length, + table_id=0, + ip_table_id=0, + is_ip4=1, + is_bind=1): + """ + """ + return self.api( + self.papi.mpls_ip_bind_unbind, + {'mb_mpls_table_id': table_id, + 'mb_label': label, + 'mb_ip_table_id': ip_table_id, + 'mb_is_bind': is_bind, + 'mb_is_ip4': is_ip4, + 'mb_address_length': dst_address_length, + 'mb_address': dst_address}) + + def mpls_tunnel_add_del( + self, + tun_sw_if_index, + next_hop_proto_is_ip4, + next_hop_address, + next_hop_sw_if_index=0xFFFFFFFF, + next_hop_table_id=0, + next_hop_weight=1, + next_hop_n_out_labels=0, + next_hop_out_label_stack=[], + next_hop_via_label=MPLS_LABEL_INVALID, + is_add=1, + l2_only=0, + is_multicast=0): + """ + + :param dst_address_length: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param dst_address: + :param next_hop_address: + :param next_hop_sw_if_index: (Default value = 0xFFFFFFFF) + :param vrf_id: (Default value = 0) + :param lookup_in_vrf: (Default value = 0) + :param classify_table_index: (Default value = 0xFFFFFFFF) + :param is_add: (Default value = 1) + :param is_drop: (Default value = 0) + :param is_ipv6: (Default value = 0) + :param is_local: (Default value = 0) + :param is_classify: (Default value = 0) + :param is_multipath: (Default value = 0) + :param is_resolve_host: (Default value = 0) + :param is_resolve_attached: (Default value = 0) + :param next_hop_weight: (Default value = 1) + :param is_multicast: (Default value = 0) + + """ + return self.api( + self.papi.mpls_tunnel_add_del, + {'mt_sw_if_index': tun_sw_if_index, + 'mt_is_add': is_add, + 'mt_l2_only': l2_only, + 'mt_is_multicast': is_multicast, + 'mt_next_hop_proto_is_ip4': next_hop_proto_is_ip4, + 'mt_next_hop_weight': next_hop_weight, + 'mt_next_hop': next_hop_address, + 'mt_next_hop_n_out_labels': next_hop_n_out_labels, + 'mt_next_hop_sw_if_index': next_hop_sw_if_index, + 'mt_next_hop_table_id': next_hop_table_id, + 'mt_next_hop_out_label_stack': next_hop_out_label_stack}) + + def nat44_interface_add_del_feature( + self, + sw_if_index, + is_inside=1, + is_add=1): + """Enable/disable NAT44 feature on the interface + + :param sw_if_index: Software index of the interface + :param is_inside: 1 if inside, 0 if outside (Default value = 1) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat44_interface_add_del_feature, + {'is_add': is_add, + 'is_inside': is_inside, + 'sw_if_index': sw_if_index}) + + def nat44_interface_add_del_output_feature( + self, + sw_if_index, + is_inside=1, + is_add=1): + """Enable/disable NAT44 output feature on the interface + + :param sw_if_index: Software index of the interface + :param is_inside: 1 if inside, 0 if outside (Default value = 1) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat44_interface_add_del_output_feature, + {'is_add': is_add, + 'is_inside': is_inside, + 'sw_if_index': sw_if_index}) + + def nat44_add_del_static_mapping( + self, + local_ip, + external_ip=0, + external_sw_if_index=0xFFFFFFFF, + local_port=0, + external_port=0, + addr_only=1, + vrf_id=0, + protocol=0, + is_add=1): + """Add/delete NAT44 static mapping + + :param local_ip: Local IP address + :param external_ip: External IP address + :param external_sw_if_index: External interface instead of IP address + :param local_port: Local port number (Default value = 0) + :param external_port: External port number (Default value = 0) + :param addr_only: 1 if address only mapping, 0 if address and port + :param vrf_id: VRF ID + :param protocol: IP protocol (Default value = 0) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat44_add_del_static_mapping, + {'is_add': is_add, + 'addr_only': addr_only, + 'local_ip_address': local_ip, + 'external_ip_address': external_ip, + 'local_port': local_port, + 'external_port': external_port, + 'external_sw_if_index': external_sw_if_index, + 'vrf_id': vrf_id, + 'protocol': protocol}) + + def nat44_add_del_address_range( + self, + first_ip_address, + last_ip_address, + is_add=1, + vrf_id=0xFFFFFFFF): + """Add/del NAT44 address range + + :param first_ip_address: First IP address + :param last_ip_address: Last IP address + :param vrf_id: VRF id for the address range + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat44_add_del_address_range, + {'first_ip_address': first_ip_address, + 'last_ip_address': last_ip_address, + 'vrf_id': vrf_id, + 'is_add': is_add}) + + def nat44_address_dump(self): + """Dump NAT44 addresses + :return: Dictionary of NAT44 addresses + """ + return self.api(self.papi.nat44_address_dump, {}) + + def nat44_interface_dump(self): + """Dump interfaces with NAT44 feature + :return: Dictionary of interfaces with NAT44 feature + """ + return self.api(self.papi.nat44_interface_dump, {}) + + def nat44_interface_output_feature_dump(self): + """Dump interfaces with NAT44 output feature + :return: Dictionary of interfaces with NAT44 output feature + """ + return self.api(self.papi.nat44_interface_output_feature_dump, {}) + + def nat44_static_mapping_dump(self): + """Dump NAT44 static mappings + :return: Dictionary of NAT44 static mappings + """ + return self.api(self.papi.nat44_static_mapping_dump, {}) + + def nat_show_config(self): + """Show NAT plugin config + :return: NAT plugin config parameters + """ + return self.api(self.papi.nat_show_config, {}) + + def nat44_add_interface_addr( + self, + sw_if_index, + is_add=1): + """Add/del NAT44 address from interface + + :param sw_if_index: Software index of the interface + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api(self.papi.nat44_add_del_interface_addr, + {'is_add': is_add, 'sw_if_index': sw_if_index}) + + def nat44_interface_addr_dump(self): + """Dump NAT44 addresses interfaces + :return: Dictionary of NAT44 addresses interfaces + """ + return self.api(self.papi.nat44_interface_addr_dump, {}) + + def nat_ipfix( + self, + domain_id=1, + src_port=4739, + enable=1): + """Enable/disable NAT IPFIX logging + + :param domain_id: Observation domain ID (Default value = 1) + :param src_port: Source port number (Default value = 4739) + :param enable: 1 if enable, 0 if disable (Default value = 1) + """ + return self.api( + self.papi.nat_ipfix_enable_disable, + {'domain_id': domain_id, + 'src_port': src_port, + 'enable': enable}) + + def nat44_user_session_dump( + self, + ip_address, + vrf_id): + """Dump NAT44 user's sessions + + :param ip_address: ip adress of the user to be dumped + :param cpu_index: cpu_index on which the user is + :param vrf_id: VRF ID + :return: Dictionary of S-NAT sessions + """ + return self.api( + self.papi.nat44_user_session_dump, + {'ip_address': ip_address, + 'vrf_id': vrf_id}) + + def nat44_user_dump(self): + """Dump NAT44 users + + :return: Dictionary of NAT44 users + """ + return self.api(self.papi.nat44_user_dump, {}) + + def nat44_add_del_lb_static_mapping( + self, + external_addr, + external_port, + protocol, + vrf_id=0, + local_num=0, + locals=[], + is_add=1): + """Add/delete NAT44 load balancing static mapping + + :param is_add - 1 if add, 0 if delete + """ + return self.api( + self.papi.nat44_add_del_lb_static_mapping, + {'is_add': is_add, + 'external_addr': external_addr, + 'external_port': external_port, + 'protocol': protocol, + 'vrf_id': vrf_id, + 'local_num': local_num, + 'locals': locals}) + + def nat44_lb_static_mapping_dump(self): + """Dump NAT44 load balancing static mappings + + :return: Dictionary of NAT44 load balancing static mapping + """ + return self.api(self.papi.nat44_lb_static_mapping_dump, {}) + + def nat_det_add_del_map( + self, + in_addr, + in_plen, + out_addr, + out_plen, + is_add=1): + """Add/delete deterministic NAT mapping + + :param is_add - 1 if add, 0 if delete + :param in_addr - inside IP address + :param in_plen - inside IP address prefix length + :param out_addr - outside IP address + :param out_plen - outside IP address prefix length + """ + return self.api( + self.papi.nat_det_add_del_map, + {'is_add': is_add, + 'is_nat44': 1, + 'in_addr': in_addr, + 'in_plen': in_plen, + 'out_addr': out_addr, + 'out_plen': out_plen}) + + def nat_det_forward( + self, + in_addr): + """Get outside address and port range from inside address + + :param in_addr - inside IP address + """ + return self.api( + self.papi.nat_det_forward, + {'in_addr': in_addr, + 'is_nat44': 1}) + + def nat_det_reverse( + self, + out_addr, + out_port): + """Get inside address from outside address and port + + :param out_addr - outside IP address + :param out_port - outside port + """ + return self.api( + self.papi.nat_det_reverse, + {'out_addr': out_addr, + 'out_port': out_port}) + + def nat_det_map_dump(self): + """Dump deterministic NAT mappings + + :return: Dictionary of deterministic NAT mappings + """ + return self.api(self.papi.nat_det_map_dump, {}) + + def nat_det_set_timeouts( + self, + udp=300, + tcp_established=7440, + tcp_transitory=240, + icmp=60): + """Set values of timeouts for deterministic NAT (in seconds) + + :param udp - UDP timeout (Default value = 300) + :param tcp_established - TCP established timeout (Default value = 7440) + :param tcp_transitory - TCP transitory timeout (Default value = 240) + :param icmp - ICMP timeout (Default value = 60) + """ + return self.api( + self.papi.nat_det_set_timeouts, + {'udp': udp, + 'tcp_established': tcp_established, + 'tcp_transitory': tcp_transitory, + 'icmp': icmp}) + + def nat_det_get_timeouts(self): + """Get values of timeouts for deterministic NAT + + :return: Timeouts for deterministic NAT (in seconds) + """ + return self.api(self.papi.nat_det_get_timeouts, {}) + + def nat_det_close_session_out( + self, + out_addr, + out_port, + ext_addr, + ext_port): + """Close deterministic NAT session using outside address and port + + :param out_addr - outside IP address + :param out_port - outside port + :param ext_addr - external host IP address + :param ext_port - external host port + """ + return self.api( + self.papi.nat_det_close_session_out, + {'out_addr': out_addr, + 'out_port': out_port, + 'ext_addr': ext_addr, + 'ext_port': ext_port}) + + def nat_det_close_session_in( + self, + in_addr, + in_port, + ext_addr, + ext_port): + """Close deterministic NAT session using inside address and port + + :param in_addr - inside IP address + :param in_port - inside port + :param ext_addr - external host IP address + :param ext_port - external host port + """ + return self.api( + self.papi.nat_det_close_session_in, + {'in_addr': in_addr, + 'in_port': in_port, + 'ext_addr': ext_addr, + 'ext_port': ext_port, + 'is_nat44': 1}) + + def nat_det_session_dump( + self, + user_addr): + """Dump deterministic NAT sessions belonging to a user + + :param user_addr - inside IP address of the user + :return: Dictionary of deterministic NAT sessions + """ + return self.api( + self.papi.nat_det_session_dump, + {'is_nat44': 1, + 'user_addr': user_addr}) + + def nat64_add_del_pool_addr_range( + self, + start_addr, + end_addr, + vrf_id=0xFFFFFFFF, + is_add=1): + """Add/del address range to NAT64 pool + + :param start_addr: First IP address + :param end_addr: Last IP address + :param vrf_id: VRF id for the address range + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat64_add_del_pool_addr_range, + {'start_addr': start_addr, + 'end_addr': end_addr, + 'vrf_id': vrf_id, + 'is_add': is_add}) + + def nat64_pool_addr_dump(self): + """Dump NAT64 pool addresses + :return: Dictionary of NAT64 pool addresses + """ + return self.api(self.papi.nat64_pool_addr_dump, {}) + + def nat64_add_del_interface( + self, + sw_if_index, + is_inside=1, + is_add=1): + """Enable/disable NAT64 feature on the interface + :param sw_if_index: Index of the interface + :param is_inside: 1 if inside, 0 if outside (Default value = 1) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat64_add_del_interface, + {'sw_if_index': sw_if_index, + 'is_inside': is_inside, + 'is_add': is_add}) + + def nat64_interface_dump(self): + """Dump interfaces with NAT64 feature + :return: Dictionary of interfaces with NAT64 feature + """ + return self.api(self.papi.nat64_interface_dump, {}) + + def nat64_add_del_static_bib( + self, + in_ip, + out_ip, + in_port, + out_port, + protocol, + vrf_id=0, + is_add=1): + """Add/delete S-NAT static BIB entry + + :param in_ip: Inside IPv6 address + :param out_ip: Outside IPv4 address + :param in_port: Inside port number + :param out_port: Outside port number + :param protocol: IP protocol + :param vrf_id: VRF ID (Default value = 0) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat64_add_del_static_bib, + {'i_addr': in_ip, + 'o_addr': out_ip, + 'i_port': in_port, + 'o_port': out_port, + 'vrf_id': vrf_id, + 'proto': protocol, + 'is_add': is_add}) + + def nat64_bib_dump(self, protocol=255): + """Dump NAT64 BIB + + :param protocol: IP protocol (Default value = 255, all BIBs) + :returns: Dictionary of NAT64 BIB entries + """ + return self.api(self.papi.nat64_bib_dump, {'proto': protocol}) + + def nat64_set_timeouts(self, udp=300, icmp=60, tcp_trans=240, tcp_est=7440, + tcp_incoming_syn=6): + """Set values of timeouts for NAT64 (in seconds) + + :param udpi: UDP timeout (Default value = 300) + :param icmp: ICMP timeout (Default value = 60) + :param tcp_trans: TCP transitory timeout (Default value = 240) + :param tcp_est: TCP established timeout (Default value = 7440) + :param tcp_incoming_syn: TCP incoming SYN timeout (Default value = 6) + """ + return self.api( + self.papi.nat64_set_timeouts, + {'udp': udp, + 'icmp': icmp, + 'tcp_trans': tcp_trans, + 'tcp_est': tcp_est, + 'tcp_incoming_syn': tcp_incoming_syn}) + + def nat64_get_timeouts(self): + """Get values of timeouts for NAT64 + + :return: Timeouts for NAT64 (in seconds) + """ + return self.api(self.papi.nat64_get_timeouts, {}) + + def nat64_st_dump(self, protocol=255): + """Dump NAT64 session table + + :param protocol: IP protocol (Default value = 255, all STs) + :returns: Dictionary of NAT64 sesstion table entries + """ + return self.api(self.papi.nat64_st_dump, {'proto': protocol}) + + def nat64_add_del_prefix(self, prefix, plen, vrf_id=0, is_add=1): + """Add/del NAT64 prefix + + :param prefix: NAT64 prefix + :param plen: NAT64 prefix length + :param vrf_id: VRF id of tenant (Default 0) + :param is_add: 1 if add, 0 if delete (Default value = 1) + """ + return self.api( + self.papi.nat64_add_del_prefix, + {'prefix': prefix, + 'prefix_len': plen, + 'vrf_id': vrf_id, + 'is_add': is_add}) + + def nat64_prefix_dump(self): + """Dump NAT64 prefix + + :returns: Dictionary of NAT64 prefixes + """ + return self.api(self.papi.nat64_prefix_dump, {}) + + def control_ping(self): + self.api(self.papi.control_ping) + + def bfd_udp_add(self, sw_if_index, desired_min_tx, required_min_rx, + detect_mult, local_addr, peer_addr, is_ipv6=0, + bfd_key_id=None, conf_key_id=None): + if bfd_key_id is None: + return self.api(self.papi.bfd_udp_add, + { + 'sw_if_index': sw_if_index, + 'desired_min_tx': desired_min_tx, + 'required_min_rx': required_min_rx, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'detect_mult': detect_mult, + }) + else: + return self.api(self.papi.bfd_udp_add, + { + 'sw_if_index': sw_if_index, + 'desired_min_tx': desired_min_tx, + 'required_min_rx': required_min_rx, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'detect_mult': detect_mult, + 'is_authenticated': 1, + 'bfd_key_id': bfd_key_id, + 'conf_key_id': conf_key_id, + }) + + def bfd_udp_mod(self, sw_if_index, desired_min_tx, required_min_rx, + detect_mult, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_mod, + { + 'sw_if_index': sw_if_index, + 'desired_min_tx': desired_min_tx, + 'required_min_rx': required_min_rx, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'detect_mult': detect_mult, + }) + + def bfd_udp_auth_activate(self, sw_if_index, local_addr, peer_addr, + is_ipv6=0, bfd_key_id=None, conf_key_id=None, + is_delayed=False): + return self.api(self.papi.bfd_udp_auth_activate, + { + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'is_delayed': 1 if is_delayed else 0, + 'bfd_key_id': bfd_key_id, + 'conf_key_id': conf_key_id, + }) + + def bfd_udp_auth_deactivate(self, sw_if_index, local_addr, peer_addr, + is_ipv6=0, is_delayed=False): + return self.api(self.papi.bfd_udp_auth_deactivate, + { + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + 'is_delayed': 1 if is_delayed else 0, + }) + + def bfd_udp_del(self, sw_if_index, local_addr, peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_del, + { + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + }) + + def bfd_udp_session_dump(self): + return self.api(self.papi.bfd_udp_session_dump, {}) + + def bfd_udp_session_set_flags(self, admin_up_down, sw_if_index, local_addr, + peer_addr, is_ipv6=0): + return self.api(self.papi.bfd_udp_session_set_flags, { + 'admin_up_down': admin_up_down, + 'sw_if_index': sw_if_index, + 'local_addr': local_addr, + 'peer_addr': peer_addr, + 'is_ipv6': is_ipv6, + }) + + def want_bfd_events(self, enable_disable=1): + return self.api(self.papi.want_bfd_events, { + 'enable_disable': enable_disable, + 'pid': os.getpid(), + }) + + def bfd_auth_set_key(self, conf_key_id, auth_type, key): + return self.api(self.papi.bfd_auth_set_key, { + 'conf_key_id': conf_key_id, + 'auth_type': auth_type, + 'key': key, + 'key_len': len(key), + }) + + def bfd_auth_del_key(self, conf_key_id): + return self.api(self.papi.bfd_auth_del_key, { + 'conf_key_id': conf_key_id, + }) + + def bfd_auth_keys_dump(self): + return self.api(self.papi.bfd_auth_keys_dump, {}) + + def bfd_udp_set_echo_source(self, sw_if_index): + return self.api(self.papi.bfd_udp_set_echo_source, + {'sw_if_index': sw_if_index}) + + def bfd_udp_del_echo_source(self): + return self.api(self.papi.bfd_udp_del_echo_source, {}) + + def classify_add_del_table( + self, + is_add, + mask, + match_n_vectors=1, + table_index=0xFFFFFFFF, + nbuckets=2, + memory_size=2097152, + skip_n_vectors=0, + next_table_index=0xFFFFFFFF, + miss_next_index=0xFFFFFFFF, + current_data_flag=0, + current_data_offset=0): + """ + :param is_add: + :param mask: + :param match_n_vectors: (Default value = 1) + :param table_index: (Default value = 0xFFFFFFFF) + :param nbuckets: (Default value = 2) + :param memory_size: (Default value = 2097152) + :param skip_n_vectors: (Default value = 0) + :param next_table_index: (Default value = 0xFFFFFFFF) + :param miss_next_index: (Default value = 0xFFFFFFFF) + :param current_data_flag: (Default value = 0) + :param current_data_offset: (Default value = 0) + """ + + return self.api( + self.papi.classify_add_del_table, + {'is_add': is_add, + 'table_index': table_index, + 'nbuckets': nbuckets, + 'memory_size': memory_size, + 'skip_n_vectors': skip_n_vectors, + 'match_n_vectors': match_n_vectors, + 'next_table_index': next_table_index, + 'miss_next_index': miss_next_index, + 'current_data_flag': current_data_flag, + 'current_data_offset': current_data_offset, + 'mask': mask}) + + def classify_add_del_session( + self, + is_add, + table_index, + match, + opaque_index=0xFFFFFFFF, + hit_next_index=0xFFFFFFFF, + advance=0, + action=0, + metadata=0): + """ + :param is_add: + :param table_index: + :param match: + :param opaque_index: (Default value = 0xFFFFFFFF) + :param hit_next_index: (Default value = 0xFFFFFFFF) + :param advance: (Default value = 0) + :param action: (Default value = 0) + :param metadata: (Default value = 0) + """ + + return self.api( + self.papi.classify_add_del_session, + {'is_add': is_add, + 'table_index': table_index, + 'hit_next_index': hit_next_index, + 'opaque_index': opaque_index, + 'advance': advance, + 'action': action, + 'metadata': metadata, + 'match': match}) + + def input_acl_set_interface( + self, + is_add, + sw_if_index, + ip4_table_index=0xFFFFFFFF, + ip6_table_index=0xFFFFFFFF, + l2_table_index=0xFFFFFFFF): + """ + :param is_add: + :param sw_if_index: + :param ip4_table_index: (Default value = 0xFFFFFFFF) + :param ip6_table_index: (Default value = 0xFFFFFFFF) + :param l2_table_index: (Default value = 0xFFFFFFFF) + """ + + return self.api( + self.papi.input_acl_set_interface, + {'sw_if_index': sw_if_index, + 'ip4_table_index': ip4_table_index, + 'ip6_table_index': ip6_table_index, + 'l2_table_index': l2_table_index, + 'is_add': is_add}) + + def set_ipfix_exporter( + self, + collector_address, + src_address, + path_mtu, + template_interval, + vrf_id=0, + collector_port=4739, + udp_checksum=0): + return self.api( + self.papi.set_ipfix_exporter, + { + 'collector_address': collector_address, + 'collector_port': collector_port, + 'src_address': src_address, + 'vrf_id': vrf_id, + 'path_mtu': path_mtu, + 'template_interval': template_interval, + 'udp_checksum': udp_checksum, + }) + + def dhcp_proxy_config(self, + dhcp_server, + dhcp_src_address, + rx_table_id=0, + server_table_id=0, + is_add=1, + is_ipv6=0): + return self.api( + self.papi.dhcp_proxy_config, + { + 'rx_vrf_id': rx_table_id, + 'server_vrf_id': server_table_id, + 'is_ipv6': is_ipv6, + 'is_add': is_add, + 'dhcp_server': dhcp_server, + 'dhcp_src_address': dhcp_src_address, + }) + + def dhcp_proxy_set_vss(self, + table_id, + fib_id, + oui, + is_add=1, + is_ip6=0): + return self.api( + self.papi.dhcp_proxy_set_vss, + { + 'tbl_id': table_id, + 'fib_id': fib_id, + 'is_ipv6': is_ip6, + 'is_add': is_add, + 'oui': oui, + }) + + def dhcp_client(self, + sw_if_index, + hostname, + client_id='', + is_add=1, + want_dhcp_events=0): + return self.api( + self.papi.dhcp_client_config, + { + 'sw_if_index': sw_if_index, + 'hostname': hostname, + 'client_id': client_id, + 'is_add': is_add, + 'want_dhcp_event': want_dhcp_events, + 'pid': os.getpid(), + }) + + def ip_mroute_add_del(self, + src_address, + grp_address, + grp_address_length, + e_flags, + next_hop_sw_if_index, + i_flags, + rpf_id=0, + table_id=0, + is_add=1, + is_ipv6=0, + is_local=0): + """ + """ + return self.api( + self.papi.ip_mroute_add_del, + {'next_hop_sw_if_index': next_hop_sw_if_index, + 'entry_flags': e_flags, + 'itf_flags': i_flags, + 'table_id': table_id, + 'rpf_id': rpf_id, + 'is_add': is_add, + 'is_ipv6': is_ipv6, + 'is_local': is_local, + 'grp_address_length': grp_address_length, + 'grp_address': grp_address, + 'src_address': src_address}) + + def mfib_signal_dump(self): + return self.api(self.papi.mfib_signal_dump, {}) + + def ip_mfib_dump(self): + return self.api(self.papi.ip_mfib_dump, {}) + + def lisp_enable_disable(self, is_enabled): + return self.api( + self.papi.lisp_enable_disable, + { + 'is_en': is_enabled, + }) + + def lisp_locator_set(self, + ls_name, + is_add=1): + return self.api( + self.papi.lisp_add_del_locator_set, + { + 'is_add': is_add, + 'locator_set_name': ls_name + }) + + def lisp_locator_set_dump(self): + return self.api(self.papi.lisp_locator_set_dump, {}) + + def lisp_locator(self, + ls_name, + sw_if_index, + priority=1, + weight=1, + is_add=1): + return self.api( + self.papi.lisp_add_del_locator, + { + 'is_add': is_add, + 'locator_set_name': ls_name, + 'sw_if_index': sw_if_index, + 'priority': priority, + 'weight': weight + }) + + def lisp_locator_dump(self, is_index_set, ls_name=None, ls_index=0): + return self.api( + self.papi.lisp_locator_dump, + { + 'is_index_set': is_index_set, + 'ls_name': ls_name, + 'ls_index': ls_index, + }) + + def lisp_local_mapping(self, + ls_name, + eid_type, + eid, + prefix_len, + vni=0, + key_id=0, + key="", + is_add=1): + return self.api( + self.papi.lisp_add_del_local_eid, + { + 'locator_set_name': ls_name, + 'is_add': is_add, + 'eid_type': eid_type, + 'eid': eid, + 'prefix_len': prefix_len, + 'vni': vni, + 'key_id': key_id, + 'key': key + }) + + def lisp_eid_table_dump(self, + eid_set=0, + prefix_length=0, + vni=0, + eid_type=0, + eid=None, + filter_opt=0): + return self.api( + self.papi.lisp_eid_table_dump, + { + 'eid_set': eid_set, + 'prefix_length': prefix_length, + 'vni': vni, + 'eid_type': eid_type, + 'eid': eid, + 'filter': filter_opt, + }) + + def lisp_remote_mapping(self, + eid_type, + eid, + eid_prefix_len=0, + vni=0, + rlocs=[], + rlocs_num=0, + is_src_dst=0, + is_add=1): + return self.api( + self.papi.lisp_add_del_remote_mapping, + { + 'is_add': is_add, + 'eid_type': eid_type, + 'eid': eid, + 'eid_len': eid_prefix_len, + 'rloc_num': rlocs_num, + 'rlocs': rlocs, + 'vni': vni, + 'is_src_dst': is_src_dst, + }) + + def lisp_adjacency(self, + leid, + reid, + leid_len, + reid_len, + eid_type, + is_add=1, + vni=0): + return self.api( + self.papi.lisp_add_del_adjacency, + { + 'is_add': is_add, + 'vni': vni, + 'eid_type': eid_type, + 'leid': leid, + 'reid': reid, + 'leid_len': leid_len, + 'reid_len': reid_len, + }) + + def lisp_adjacencies_get(self, vni=0): + return self.api( + self.papi.lisp_adjacencies_get, + { + 'vni': vni + }) + + def map_add_domain(self, + ip6_prefix, + ip6_prefix_len, + ip6_src, + ip6_src_prefix_len, + ip4_prefix, + ip4_prefix_len, + ea_bits_len=0, + psid_offset=0, + psid_length=0, + is_translation=0, + mtu=1280): + return self.api( + self.papi.map_add_domain, + { + 'ip6_prefix': ip6_prefix, + 'ip6_prefix_len': ip6_prefix_len, + 'ip4_prefix': ip4_prefix, + 'ip4_prefix_len': ip4_prefix_len, + 'ip6_src': ip6_src, + 'ip6_src_prefix_len': ip6_src_prefix_len, + 'ea_bits_len': ea_bits_len, + 'psid_offset': psid_offset, + 'psid_length': psid_length, + 'is_translation': is_translation, + 'mtu': mtu + }) + + def gtpu_add_del_tunnel( + self, + src_addr, + dst_addr, + is_add=1, + is_ipv6=0, + mcast_sw_if_index=0xFFFFFFFF, + encap_vrf_id=0, + decap_next_index=0xFFFFFFFF, + teid=0): + """ + + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param src_addr: + :param dst_addr: + :param mcast_sw_if_index: (Default value = 0xFFFFFFFF) + :param encap_vrf_id: (Default value = 0) + :param decap_next_index: (Default value = 0xFFFFFFFF) + :param teid: (Default value = 0) + + """ + return self.api(self.papi.gtpu_add_del_tunnel, + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'src_address': src_addr, + 'dst_address': dst_addr, + 'mcast_sw_if_index': mcast_sw_if_index, + 'encap_vrf_id': encap_vrf_id, + 'decap_next_index': decap_next_index, + 'teid': teid}) + + def vxlan_gpe_add_del_tunnel( + self, + src_addr, + dst_addr, + mcast_sw_if_index=0xFFFFFFFF, + is_add=1, + is_ipv6=0, + encap_vrf_id=0, + decap_vrf_id=0, + protocol=3, + vni=0): + """ + + :param local: + :param remote: + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param encap_vrf_id: (Default value = 0) + :param decap_vrf_id: (Default value = 0) + :param mcast_sw_if_index: (Default value = 0xFFFFFFFF) + :param protocol: (Default value = 3) + :param vni: (Default value = 0) + + """ + return self.api(self.papi.vxlan_gpe_add_del_tunnel, + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'local': src_addr, + 'remote': dst_addr, + 'mcast_sw_if_index': mcast_sw_if_index, + 'encap_vrf_id': encap_vrf_id, + 'decap_vrf_id': decap_vrf_id, + 'protocol': protocol, + 'vni': vni}) + + def pppoe_add_del_session( + self, + client_ip, + client_mac, + session_id=0, + is_add=1, + is_ipv6=0, + decap_vrf_id=0): + """ + + :param is_add: (Default value = 1) + :param is_ipv6: (Default value = 0) + :param client_ip: + :param session_id: (Default value = 0) + :param client_mac: + :param decap_vrf_id: (Default value = 0) + + """ + return self.api(self.papi.pppoe_add_del_session, + {'is_add': is_add, + 'is_ipv6': is_ipv6, + 'session_id': session_id, + 'client_ip': client_ip, + 'decap_vrf_id': decap_vrf_id, + 'client_mac': client_mac}) + + def sr_localsid_add_del(self, + localsid_addr, + behavior, + nh_addr, + is_del=0, + end_psp=0, + sw_if_index=0xFFFFFFFF, + vlan_index=0, + fib_table=0, + ): + """ Add/del IPv6 SR local-SID. + + :param localsid_addr: + :param behavior: END=1; END.X=2; END.DX2=4; END.DX6=5; + :param behavior: END.DX4=6; END.DT6=7; END.DT4=8 + :param nh_addr: + :param is_del: (Default value = 0) + :param end_psp: (Default value = 0) + :param sw_if_index: (Default value = 0xFFFFFFFF) + :param vlan_index: (Default value = 0) + :param fib_table: (Default value = 0) + """ + return self.api( + self.papi.sr_localsid_add_del, + {'is_del': is_del, + 'localsid_addr': localsid_addr, + 'end_psp': end_psp, + 'behavior': behavior, + 'sw_if_index': sw_if_index, + 'vlan_index': vlan_index, + 'fib_table': fib_table, + 'nh_addr': nh_addr + } + ) + + def sr_policy_add( + self, + bsid_addr, + weight=1, + is_encap=1, + type=0, + fib_table=0, + n_segments=0, + segments=[]): + """ + :param bsid_addr: bindingSID of the SR Policy + :param weight: weight of the sid list. optional. (default: 1) + :param is_encap: (bool) whether SR policy should Encap or SRH insert \ + (default: Encap) + :param type: type/behavior of the SR policy. (default or spray) \ + (default: default) + :param fib_table: VRF where to install the FIB entry for the BSID \ + (default: 0) + :param n_segments: number of segments \ + (default: 0) + :param segments: a vector of IPv6 address composing the segment list \ + (default: []) + """ + return self.api( + self.papi.sr_policy_add, + {'bsid_addr': bsid_addr, + 'weight': weight, + 'is_encap': is_encap, + 'type': type, + 'fib_table': fib_table, + 'n_segments': n_segments, + 'segments': segments + } + ) + + def sr_policy_del( + self, + bsid_addr, + sr_policy_index=0): + """ + :param bsid: bindingSID of the SR Policy + :param sr_policy_index: index of the sr policy (default: 0) + """ + return self.api( + self.papi.sr_policy_del, + {'bsid_addr': bsid_addr, + 'sr_policy_index': sr_policy_index + }) + + def sr_steering_add_del( + self, + is_del, + bsid_addr, + sr_policy_index, + table_id, + prefix_addr, + mask_width, + sw_if_index, + traffic_type): + """ + Steer traffic L2 and L3 traffic through a given SR policy + + :param is_del: delete or add + :param bsid_addr: bindingSID of the SR Policy (alt to sr_policy_index) + :param sr_policy: is the index of the SR Policy (alt to bsid) + :param table_id: is the VRF where to install the FIB entry for the BSID + :param prefix_addr: is the IPv4/v6 address for L3 traffic type + :param mask_width: is the mask for L3 traffic type + :param sw_if_index: is the incoming interface for L2 traffic + :param traffic_type: type of traffic (IPv4: 4, IPv6: 6, L2: 2) + """ + return self.api( + self.papi.sr_steering_add_del, + {'is_del': is_del, + 'bsid_addr': bsid_addr, + 'sr_policy_index': sr_policy_index, + 'table_id': table_id, + 'prefix_addr': prefix_addr, + 'mask_width': mask_width, + 'sw_if_index': sw_if_index, + 'traffic_type': traffic_type + }) + + def acl_add_replace(self, acl_index, r, tag='', + expected_retval=0): + """Add/replace an ACL + :param int acl_index: ACL index to replace, 2^32-1 to create new ACL. + :param acl_rule r: ACL rules array. + :param str tag: symbolic tag (description) for this ACL. + :param int count: number of rules. + """ + return self.api(self.papi.acl_add_replace, + {'acl_index': acl_index, + 'r': r, + 'count': len(r), + 'tag': tag}, + expected_retval=expected_retval) + + def acl_del(self, acl_index, expected_retval=0): + """ + + :param acl_index: + :return: + """ + return self.api(self.papi.acl_del, + {'acl_index': acl_index}, + expected_retval=expected_retval) + + def acl_interface_set_acl_list(self, sw_if_index, n_input, acls, + expected_retval=0): + return self.api(self.papi.acl_interface_set_acl_list, + {'sw_if_index': sw_if_index, + 'count': len(acls), + 'n_input': n_input, + 'acls': acls}, + expected_retval=expected_retval) + + def acl_dump(self, acl_index, expected_retval=0): + return self.api(self.papi.acl_dump, + {'acl_index': acl_index}, + expected_retval=expected_retval) + + def macip_acl_add(self, rules, tag=""): + """ Add MACIP acl + + :param rules: list of rules for given acl + :param tag: acl tag + """ + + return self.api(self.papi.macip_acl_add, + {'r': rules, + 'count': len(rules), + 'tag': tag}) + + def macip_acl_add_replace(self, rules, acl_index=0xFFFFFFFF, tag=""): + """ Add MACIP acl + + :param rules: list of rules for given acl + :param tag: acl tag + """ + + return self.api(self.papi.macip_acl_add_replace, + {'acl_index': acl_index, + 'r': rules, + 'count': len(rules), + 'tag': tag}) + + def macip_acl_del(self, acl_index): + """ + + :param acl_index: + :return: + """ + return self.api(self.papi.macip_acl_del, + {'acl_index': acl_index}) + + def macip_acl_interface_add_del(self, + sw_if_index, + acl_index, + is_add=1): + """ Add MACIP acl to interface + + :param sw_if_index: + :param acl_index: + :param is_add: (Default value = 1) + """ + + return self.api(self.papi.macip_acl_interface_add_del, + {'is_add': is_add, + 'sw_if_index': sw_if_index, + 'acl_index': acl_index}) + + def macip_acl_interface_get(self): + """ Return interface acls dump + """ + return self.api( + self.papi.macip_acl_interface_get, {}) + + def macip_acl_dump(self, acl_index=4294967295): + """ Return MACIP acl dump + """ + + return self.api( + self.papi.macip_acl_dump, {'acl_index': acl_index}) diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py new file mode 100644 index 00000000..81e9714a --- /dev/null +++ b/test/vpp_pg_interface.py @@ -0,0 +1,481 @@ +import os +import time +import socket +import struct +from traceback import format_exc, format_stack +from scapy.utils import wrpcap, rdpcap, PcapReader +from scapy.plist import PacketList +from vpp_interface import VppInterface + +from scapy.layers.l2 import Ether, ARP +from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\ + ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr, ICMPv6ND_RA, RouterAlert, \ + IPv6ExtHdrHopByHop +from util import ppp, ppc +from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ismaddr +from scapy.utils import inet_pton, inet_ntop + + +class CaptureTimeoutError(Exception): + """ Exception raised if capture or packet doesn't appear within timeout """ + pass + + +def is_ipv6_misc(p): + """ Is packet one of uninteresting IPv6 broadcasts? """ + if p.haslayer(ICMPv6ND_RA): + if in6_ismaddr(p[IPv6].dst): + return True + if p.haslayer(IPv6ExtHdrHopByHop): + for o in p[IPv6ExtHdrHopByHop].options: + if isinstance(o, RouterAlert): + return True + return False + + +class VppPGInterface(VppInterface): + """ + VPP packet-generator interface + """ + + @property + def pg_index(self): + """packet-generator interface index assigned by VPP""" + return self._pg_index + + @property + def out_path(self): + """pcap file path - captured packets""" + return self._out_path + + @property + def in_path(self): + """ pcap file path - injected packets""" + return self._in_path + + @property + def capture_cli(self): + """CLI string to start capture on this interface""" + return self._capture_cli + + @property + def cap_name(self): + """capture name for this interface""" + return self._cap_name + + @property + def input_cli(self): + """CLI string to load the injected packets""" + return self._input_cli + + @property + def in_history_counter(self): + """Self-incrementing counter used when renaming old pcap files""" + v = self._in_history_counter + self._in_history_counter += 1 + return v + + @property + def out_history_counter(self): + """Self-incrementing counter used when renaming old pcap files""" + v = self._out_history_counter + self._out_history_counter += 1 + return v + + def __init__(self, test, pg_index): + """ Create VPP packet-generator interface """ + r = test.vapi.pg_create_interface(pg_index) + self._sw_if_index = r.sw_if_index + + super(VppPGInterface, self).__init__(test) + + self._in_history_counter = 0 + self._out_history_counter = 0 + self._out_assert_counter = 0 + self._pg_index = pg_index + self._out_file = "pg%u_out.pcap" % self.pg_index + self._out_path = self.test.tempdir + "/" + self._out_file + self._in_file = "pg%u_in.pcap" % self.pg_index + self._in_path = self.test.tempdir + "/" + self._in_file + self._capture_cli = "packet-generator capture pg%u pcap %s" % ( + self.pg_index, self.out_path) + self._cap_name = "pcap%u" % self.sw_if_index + self._input_cli = \ + "packet-generator new pcap %s source pg%u name %s" % ( + self.in_path, self.pg_index, self.cap_name) + + def enable_capture(self): + """ Enable capture on this packet-generator interface""" + try: + if os.path.isfile(self.out_path): + name = "%s/history.[timestamp:%f].[%s-counter:%04d].%s" % \ + (self.test.tempdir, + time.time(), + self.name, + self.out_history_counter, + self._out_file) + self.test.logger.debug("Renaming %s->%s" % + (self.out_path, name)) + os.rename(self.out_path, name) + except: + pass + # FIXME this should be an API, but no such exists atm + self.test.vapi.cli(self.capture_cli) + self._pcap_reader = None + + def add_stream(self, pkts): + """ + Add a stream of packets to this packet-generator + + :param pkts: iterable packets + + """ + try: + if os.path.isfile(self.in_path): + name = "%s/history.[timestamp:%f].[%s-counter:%04d].%s" %\ + (self.test.tempdir, + time.time(), + self.name, + self.in_history_counter, + self._in_file) + self.test.logger.debug("Renaming %s->%s" % + (self.in_path, name)) + os.rename(self.in_path, name) + except: + pass + wrpcap(self.in_path, pkts) + self.test.register_capture(self.cap_name) + # FIXME this should be an API, but no such exists atm + self.test.vapi.cli(self.input_cli) + + def generate_debug_aid(self, kind): + """ Create a hardlink to the out file with a counter and a file + containing stack trace to ease debugging in case of multiple capture + files present. """ + self.test.logger.debug("Generating debug aid for %s on %s" % + (kind, self._name)) + link_path, stack_path = ["%s/debug_%s_%s_%s.%s" % + (self.test.tempdir, self._name, + self._out_assert_counter, kind, suffix) + for suffix in ["pcap", "stack"] + ] + os.link(self.out_path, link_path) + with open(stack_path, "w") as f: + f.writelines(format_stack()) + self._out_assert_counter += 1 + + def _get_capture(self, timeout, filter_out_fn=is_ipv6_misc): + """ Helper method to get capture and filter it """ + try: + if not self.wait_for_capture_file(timeout): + return None + output = rdpcap(self.out_path) + self.test.logger.debug("Capture has %s packets" % len(output.res)) + except: + self.test.logger.debug("Exception in scapy.rdpcap (%s): %s" % + (self.out_path, format_exc())) + return None + before = len(output.res) + if filter_out_fn: + output.res = [p for p in output.res if not filter_out_fn(p)] + removed = before - len(output.res) + if removed: + self.test.logger.debug( + "Filtered out %s packets from capture (returning %s)" % + (removed, len(output.res))) + return output + + def get_capture(self, expected_count=None, remark=None, timeout=1, + filter_out_fn=is_ipv6_misc): + """ Get captured packets + + :param expected_count: expected number of packets to capture, if None, + then self.test.packet_count_for_dst_pg_idx is + used to lookup the expected count + :param remark: remark printed into debug logs + :param timeout: how long to wait for packets + :param filter_out_fn: filter applied to each packet, packets for which + the filter returns True are removed from capture + :returns: iterable packets + """ + remaining_time = timeout + capture = None + name = self.name if remark is None else "%s (%s)" % (self.name, remark) + based_on = "based on provided argument" + if expected_count is None: + expected_count = \ + self.test.get_packet_count_for_if_idx(self.sw_if_index) + based_on = "based on stored packet_infos" + if expected_count == 0: + raise Exception( + "Internal error, expected packet count for %s is 0!" % + name) + self.test.logger.debug("Expecting to capture %s (%s) packets on %s" % ( + expected_count, based_on, name)) + while remaining_time > 0: + before = time.time() + capture = self._get_capture(remaining_time, filter_out_fn) + elapsed_time = time.time() - before + if capture: + if len(capture.res) == expected_count: + # bingo, got the packets we expected + return capture + elif len(capture.res) > expected_count: + self.test.logger.error( + ppc("Unexpected packets captured:", capture)) + break + else: + self.test.logger.debug("Partial capture containing %s " + "packets doesn't match expected " + "count %s (yet?)" % + (len(capture.res), expected_count)) + elif expected_count == 0: + # bingo, got None as we expected - return empty capture + return PacketList() + remaining_time -= elapsed_time + if capture: + self.generate_debug_aid("count-mismatch") + raise Exception("Captured packets mismatch, captured %s packets, " + "expected %s packets on %s" % + (len(capture.res), expected_count, name)) + else: + raise Exception("No packets captured on %s" % name) + + def assert_nothing_captured(self, remark=None, filter_out_fn=is_ipv6_misc): + """ Assert that nothing unfiltered was captured on interface + + :param remark: remark printed into debug logs + :param filter_out_fn: filter applied to each packet, packets for which + the filter returns True are removed from capture + """ + if os.path.isfile(self.out_path): + try: + capture = self.get_capture( + 0, remark=remark, filter_out_fn=filter_out_fn) + if not capture or len(capture.res) == 0: + # junk filtered out, we're good + return + except: + pass + self.generate_debug_aid("empty-assert") + if remark: + raise AssertionError( + "Non-empty capture file present for interface %s (%s)" % + (self.name, remark)) + else: + raise AssertionError("Capture file present for interface %s" % + self.name) + + def wait_for_capture_file(self, timeout=1): + """ + Wait until pcap capture file appears + + :param timeout: How long to wait for the packet (default 1s) + + :returns: True/False if the file is present or appears within timeout + """ + deadline = time.time() + timeout + if not os.path.isfile(self.out_path): + self.test.logger.debug("Waiting for capture file %s to appear, " + "timeout is %ss" % (self.out_path, timeout)) + else: + self.test.logger.debug("Capture file %s already exists" % + self.out_path) + return True + while time.time() < deadline: + if os.path.isfile(self.out_path): + break + time.sleep(0) # yield + if os.path.isfile(self.out_path): + self.test.logger.debug("Capture file appeared after %fs" % + (time.time() - (deadline - timeout))) + else: + self.test.logger.debug("Timeout - capture file still nowhere") + return False + return True + + def verify_enough_packet_data_in_pcap(self): + """ + Check if enough data is available in file handled by internal pcap + reader so that a whole packet can be read. + + :returns: True if enough data present, else False + """ + orig_pos = self._pcap_reader.f.tell() # save file position + enough_data = False + # read packet header from pcap + packet_header_size = 16 + caplen = None + end_pos = None + hdr = self._pcap_reader.f.read(packet_header_size) + if len(hdr) == packet_header_size: + # parse the capture length - caplen + sec, usec, caplen, wirelen = struct.unpack( + self._pcap_reader.endian + "IIII", hdr) + self._pcap_reader.f.seek(0, 2) # seek to end of file + end_pos = self._pcap_reader.f.tell() # get position at end + if end_pos >= orig_pos + len(hdr) + caplen: + enough_data = True # yay, we have enough data + self._pcap_reader.f.seek(orig_pos, 0) # restore original position + return enough_data + + def wait_for_packet(self, timeout, filter_out_fn=is_ipv6_misc): + """ + Wait for next packet captured with a timeout + + :param timeout: How long to wait for the packet + + :returns: Captured packet if no packet arrived within timeout + :raises Exception: if no packet arrives within timeout + """ + deadline = time.time() + timeout + if self._pcap_reader is None: + if not self.wait_for_capture_file(timeout): + raise CaptureTimeoutError("Capture file %s did not appear " + "within timeout" % self.out_path) + while time.time() < deadline: + try: + self._pcap_reader = PcapReader(self.out_path) + break + except: + self.test.logger.debug( + "Exception in scapy.PcapReader(%s): %s" % + (self.out_path, format_exc())) + if not self._pcap_reader: + raise CaptureTimeoutError("Capture file %s did not appear within " + "timeout" % self.out_path) + + poll = False + if timeout > 0: + self.test.logger.debug("Waiting for packet") + else: + poll = True + self.test.logger.debug("Polling for packet") + while time.time() < deadline or poll: + if not self.verify_enough_packet_data_in_pcap(): + time.sleep(0) # yield + poll = False + continue + p = self._pcap_reader.recv() + if p is not None: + if filter_out_fn is not None and filter_out_fn(p): + self.test.logger.debug( + "Packet received after %ss was filtered out" % + (time.time() - (deadline - timeout))) + else: + self.test.logger.debug( + "Packet received after %fs" % + (time.time() - (deadline - timeout))) + return p + time.sleep(0) # yield + poll = False + self.test.logger.debug("Timeout - no packets received") + raise CaptureTimeoutError("Packet didn't arrive within timeout") + + def create_arp_req(self): + """Create ARP request applicable for this interface""" + return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / + ARP(op=ARP.who_has, pdst=self.local_ip4, + psrc=self.remote_ip4, hwsrc=self.remote_mac)) + + def create_ndp_req(self): + """Create NDP - NS applicable for this interface""" + nsma = in6_getnsma(inet_pton(socket.AF_INET6, self.local_ip6)) + d = inet_ntop(socket.AF_INET6, nsma) + + return (Ether(dst=in6_getnsmac(nsma)) / + IPv6(dst=d, src=self.remote_ip6) / + ICMPv6ND_NS(tgt=self.local_ip6) / + ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac)) + + def resolve_arp(self, pg_interface=None): + """Resolve ARP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + + """ + if pg_interface is None: + pg_interface = self + self.test.logger.info("Sending ARP request for %s on port %s" % + (self.local_ip4, pg_interface.name)) + arp_req = self.create_arp_req() + pg_interface.add_stream(arp_req) + pg_interface.enable_capture() + self.test.pg_start() + self.test.logger.info(self.test.vapi.cli("show trace")) + try: + captured_packet = pg_interface.wait_for_packet(1) + except: + self.test.logger.info("No ARP received on port %s" % + pg_interface.name) + return + arp_reply = captured_packet.copy() # keep original for exception + # Make Dot1AD packet content recognizable to scapy + if arp_reply.type == 0x88a8: + arp_reply.type = 0x8100 + arp_reply = Ether(str(arp_reply)) + try: + if arp_reply[ARP].op == ARP.is_at: + self.test.logger.info("VPP %s MAC address is %s " % + (self.name, arp_reply[ARP].hwsrc)) + self._local_mac = arp_reply[ARP].hwsrc + else: + self.test.logger.info("No ARP received on port %s" % + pg_interface.name) + except: + self.test.logger.error( + ppp("Unexpected response to ARP request:", captured_packet)) + raise + + def resolve_ndp(self, pg_interface=None, timeout=1): + """Resolve NDP using provided packet-generator interface + + :param pg_interface: interface used to resolve, if None then this + interface is used + :param timeout: how long to wait for response before giving up + + """ + if pg_interface is None: + pg_interface = self + self.test.logger.info("Sending NDP request for %s on port %s" % + (self.local_ip6, pg_interface.name)) + ndp_req = self.create_ndp_req() + pg_interface.add_stream(ndp_req) + pg_interface.enable_capture() + self.test.pg_start() + now = time.time() + deadline = now + timeout + # Enabling IPv6 on an interface can generate more than the + # ND reply we are looking for (namely MLD). So loop through + # the replies to look for want we want. + while now < deadline: + try: + captured_packet = pg_interface.wait_for_packet( + deadline - now, filter_out_fn=None) + except: + self.test.logger.error( + "Timeout while waiting for NDP response") + raise + ndp_reply = captured_packet.copy() # keep original for exception + # Make Dot1AD packet content recognizable to scapy + if ndp_reply.type == 0x88a8: + ndp_reply.type = 0x8100 + ndp_reply = Ether(str(ndp_reply)) + try: + ndp_na = ndp_reply[ICMPv6ND_NA] + opt = ndp_na[ICMPv6NDOptDstLLAddr] + self.test.logger.info("VPP %s MAC address is %s " % + (self.name, opt.lladdr)) + self._local_mac = opt.lladdr + self.test.logger.debug(self.test.vapi.cli("show trace")) + # we now have the MAC we've been after + return + except: + self.test.logger.info( + ppp("Unexpected response to NDP request:", + captured_packet)) + now = time.time() + + self.test.logger.debug(self.test.vapi.cli("show trace")) + raise Exception("Timeout while waiting for NDP response") diff --git a/test/vpp_pppoe_interface.py b/test/vpp_pppoe_interface.py new file mode 100644 index 00000000..9a8b8699 --- /dev/null +++ b/test/vpp_pppoe_interface.py @@ -0,0 +1,79 @@ + +from vpp_interface import VppInterface +import socket +from util import ppp, ppc, mactobinary + + +class VppPppoeInterface(VppInterface): + """ + VPP Pppoe interface + """ + + def __init__(self, test, client_ip, client_mac, + session_id, decap_vrf_id=0): + """ Create VPP PPPoE4 interface """ + self._sw_if_index = 0 + super(VppPppoeInterface, self).__init__(test) + self._test = test + self.client_ip = client_ip + self.client_mac = client_mac + self.session_id = session_id + self.decap_vrf_id = decap_vrf_id + + def add_vpp_config(self): + cip = socket.inet_pton(socket.AF_INET, self.client_ip) + cmac = mactobinary(self.client_mac) + r = self.test.vapi.pppoe_add_del_session( + cip, cmac, + session_id=self.session_id, + decap_vrf_id=self.decap_vrf_id) + self._sw_if_index = r.sw_if_index + self.generate_remote_hosts() + + def remove_vpp_config(self): + cip = socket.inet_pton(socket.AF_INET, self.client_ip) + cmac = mactobinary(self.client_mac) + self.unconfig() + r = self.test.vapi.pppoe_add_del_session( + cip, cmac, + session_id=self.session_id, + decap_vrf_id=self.decap_vrf_id, + is_add=0) + + +class VppPppoe6Interface(VppInterface): + """ + VPP Pppoe IPv6 interface + """ + + def __init__(self, test, src_ip, dst_ip, outer_fib_id=0, is_teb=0): + """ Create VPP PPPoE6 interface """ + self._sw_if_index = 0 + super(VppPppoe6Interface, self).__init__(test) + self._test = test + self.client_ip = client_ip + self.client_mac = client_mac + self.session_id = session_id + self.decap_vrf_id = decap_vrf_id + + def add_vpp_config(self): + cip = socket.inet_pton(socket.AF_INET6, self.client_ip) + cmac = mactobinary(self.client_mac) + r = self.test.vapi.pppoe_add_del_session( + cip, cmac, + session_id=self.session_id, + decap_vrf_id=self.decap_vrf_id, + is_ip6=1) + self._sw_if_index = r.sw_if_index + self.generate_remote_hosts() + + def remove_vpp_config(self): + cip = socket.inet_pton(socket.AF_INET6, self.client_ip) + cmac = mactobinary(self.client_mac) + self.unconfig() + r = self.test.vapi.pppoe_add_del_session( + cip, cmac, + session_id=self.session_id, + decap_vrf_id=self.decap_vrf_id, + is_add=0, + is_ip6=1) diff --git a/test/vpp_srv6.py b/test/vpp_srv6.py new file mode 100644 index 00000000..28ff4b85 --- /dev/null +++ b/test/vpp_srv6.py @@ -0,0 +1,238 @@ +""" + SRv6 LocalSIDs + + object abstractions for representing SRv6 localSIDs in VPP +""" + +from vpp_object import * +from socket import inet_pton, inet_ntop, AF_INET, AF_INET6 + + +class SRv6LocalSIDBehaviors(): + # from src/vnet/srv6/sr.h + SR_BEHAVIOR_END = 1 + SR_BEHAVIOR_X = 2 + SR_BEHAVIOR_T = 3 + SR_BEHAVIOR_D_FIRST = 4 # Unused. Separator in between regular and D + SR_BEHAVIOR_DX2 = 5 + SR_BEHAVIOR_DX6 = 6 + SR_BEHAVIOR_DX4 = 7 + SR_BEHAVIOR_DT6 = 8 + SR_BEHAVIOR_DT4 = 9 + SR_BEHAVIOR_LAST = 10 # Must always be the last one + + +class SRv6PolicyType(): + # from src/vnet/srv6/sr.h + SR_POLICY_TYPE_DEFAULT = 0 + SR_POLICY_TYPE_SPRAY = 1 + + +class SRv6PolicySteeringTypes(): + # from src/vnet/srv6/sr.h + SR_STEER_L2 = 2 + SR_STEER_IPV4 = 4 + SR_STEER_IPV6 = 6 + + +class VppSRv6LocalSID(VppObject): + """ + SRv6 LocalSID + """ + + def __init__(self, test, localsid_addr, behavior, nh_addr, end_psp, + sw_if_index, vlan_index, fib_table): + self._test = test + self.localsid_addr = localsid_addr + # keep binary format in _localsid_addr + self._localsid_addr = inet_pton(AF_INET6, self.localsid_addr) + self.behavior = behavior + self.nh_addr = nh_addr + # keep binary format in _nh_addr + if ':' in nh_addr: + # IPv6 + self._nh_addr = inet_pton(AF_INET6, nh_addr) + else: + # IPv4 + # API expects 16 octets (128 bits) + # last 4 octets are used for IPv4 + # --> prepend 12 octets + self._nh_addr = ('\x00' * 12) + inet_pton(AF_INET, nh_addr) + self.end_psp = end_psp + self.sw_if_index = sw_if_index + self.vlan_index = vlan_index + self.fib_table = fib_table + self._configured = False + + def add_vpp_config(self): + self._test.vapi.sr_localsid_add_del( + self._localsid_addr, + self.behavior, + self._nh_addr, + is_del=0, + end_psp=self.end_psp, + sw_if_index=self.sw_if_index, + vlan_index=self.vlan_index, + fib_table=self.fib_table) + self._configured = True + + def remove_vpp_config(self): + self._test.vapi.sr_localsid_add_del( + self._localsid_addr, + self.behavior, + self._nh_addr, + is_del=1, + end_psp=self.end_psp, + sw_if_index=self.sw_if_index, + vlan_index=self.vlan_index, + fib_table=self.fib_table) + self._configured = False + + def query_vpp_config(self): + # sr_localsids_dump API is disabled + # use _configured flag for now + return self._configured + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d;%s,%d" + % (self.fib_table, + self.localsid_addr, + self.behavior)) + + +class VppSRv6Policy(VppObject): + """ + SRv6 Policy + """ + + def __init__(self, test, bsid, + is_encap, sr_type, weight, fib_table, + segments, source): + self._test = test + self.bsid = bsid + # keep binary format in _bsid + self._bsid = inet_pton(AF_INET6, bsid) + self.is_encap = is_encap + self.sr_type = sr_type + self.weight = weight + self.fib_table = fib_table + self.segments = segments + # keep binary format in _segments + self._segments = [] + for seg in segments: + self._segments.extend(inet_pton(AF_INET6, seg)) + self.n_segments = len(segments) + # source not passed to API + # self.source = inet_pton(AF_INET6, source) + self.source = source + self._configured = False + + def add_vpp_config(self): + self._test.vapi.sr_policy_add( + self._bsid, + self.weight, + self.is_encap, + self.sr_type, + self.fib_table, + self.n_segments, + self._segments) + self._configured = True + + def remove_vpp_config(self): + self._test.vapi.sr_policy_del( + self._bsid) + self._configured = False + + def query_vpp_config(self): + # no API to query SR Policies + # use _configured flag for now + return self._configured + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d;%s-><%s>;%d" + % (self.sr_type, + self.bsid, + ','.join(self.segments), + self.is_encap)) + + +class VppSRv6Steering(VppObject): + """ + SRv6 Steering + """ + + def __init__(self, test, + bsid, + prefix, + mask_width, + traffic_type, + sr_policy_index, + table_id, + sw_if_index): + self._test = test + self.bsid = bsid + # keep binary format in _bsid + self._bsid = inet_pton(AF_INET6, bsid) + self.prefix = prefix + # keep binary format in _prefix + if ':' in prefix: + # IPv6 + self._prefix = inet_pton(AF_INET6, prefix) + else: + # IPv4 + # API expects 16 octets (128 bits) + # last 4 octets are used for IPv4 + # --> prepend 12 octets + self._prefix = ('\x00' * 12) + inet_pton(AF_INET, prefix) + self.mask_width = mask_width + self.traffic_type = traffic_type + self.sr_policy_index = sr_policy_index + self.sw_if_index = sw_if_index + self.table_id = table_id + self._configured = False + + def add_vpp_config(self): + self._test.vapi.sr_steering_add_del( + 0, + self._bsid, + self.sr_policy_index, + self.table_id, + self._prefix, + self.mask_width, + self.sw_if_index, + self.traffic_type) + self._configured = True + + def remove_vpp_config(self): + self._test.vapi.sr_steering_add_del( + 1, + self._bsid, + self.sr_policy_index, + self.table_id, + self._prefix, + self.mask_width, + self.sw_if_index, + self.traffic_type) + self._configured = False + + def query_vpp_config(self): + # no API to query steering entries + # use _configured flag for now + return self._configured + + def __str__(self): + return self.object_id() + + def object_id(self): + return ("%d;%d;%s/%d->%s" + % (self.table_id, + self.traffic_type, + self.prefix, + self.mask_width, + self.bsid)) diff --git a/test/vpp_sub_interface.py b/test/vpp_sub_interface.py new file mode 100644 index 00000000..cabee88d --- /dev/null +++ b/test/vpp_sub_interface.py @@ -0,0 +1,213 @@ +from scapy.layers.l2 import Dot1Q +from abc import abstractmethod, ABCMeta +from vpp_interface import VppInterface +from vpp_pg_interface import VppPGInterface +from vpp_papi_provider import L2_VTR_OP + + +class VppSubInterface(VppPGInterface): + __metaclass__ = ABCMeta + + @property + def parent(self): + """Parent interface for this sub-interface""" + return self._parent + + @property + def sub_id(self): + """Sub-interface ID""" + return self._sub_id + + @property + def tag1(self): + return self._tag1 + + @property + def tag2(self): + return self._tag2 + + @property + def vtr(self): + return self._vtr + + def __init__(self, test, parent, sub_id): + VppInterface.__init__(self, test) + self._parent = parent + self._parent.add_sub_if(self) + self._sub_id = sub_id + self.set_vtr(L2_VTR_OP.L2_DISABLED) + self.DOT1AD_TYPE = 0x88A8 + self.DOT1Q_TYPE = 0x8100 + + @abstractmethod + def create_arp_req(self): + pass + + @abstractmethod + def create_ndp_req(self): + pass + + def resolve_arp(self): + super(VppSubInterface, self).resolve_arp(self.parent) + + def resolve_ndp(self): + super(VppSubInterface, self).resolve_ndp(self.parent) + + @abstractmethod + def add_dot1_layer(self, pkt): + pass + + def remove_vpp_config(self): + self.test.vapi.delete_subif(self._sw_if_index) + + def _add_tag(self, packet, vlan, tag_type): + payload = packet.payload + inner_type = packet.type + packet.remove_payload() + packet.add_payload(Dot1Q(vlan=vlan) / payload) + packet.payload.type = inner_type + packet.payload.vlan = vlan + packet.type = tag_type + return packet + + def _remove_tag(self, packet, vlan=None, tag_type=None): + if tag_type: + self.test.instance().assertEqual(packet.type, tag_type) + + payload = packet.payload + if vlan: + self.test.instance().assertEqual(payload.vlan, vlan) + inner_type = payload.type + payload = payload.payload + packet.remove_payload() + packet.add_payload(payload) + packet.type = inner_type + return packet + + def add_dot1q_layer(self, packet, vlan): + return self._add_tag(packet, vlan, self.DOT1Q_TYPE) + + def add_dot1ad_layer(self, packet, outer, inner): + p = self._add_tag(packet, inner, self.DOT1Q_TYPE) + return self._add_tag(p, outer, self.DOT1AD_TYPE) + + def remove_dot1q_layer(self, packet, vlan=None): + return self._remove_tag(packet, vlan, self.DOT1Q_TYPE) + + def remove_dot1ad_layer(self, packet, outer=None, inner=None): + p = self._remove_tag(packet, outer, self.DOT1AD_TYPE) + return self._remove_tag(p, inner, self.DOT1Q_TYPE) + + def set_vtr(self, vtr, push1q=0, tag=None, inner=None, outer=None): + self._tag1 = 0 + self._tag2 = 0 + self._push1q = 0 + + if (vtr == L2_VTR_OP.L2_PUSH_1 or + vtr == L2_VTR_OP.L2_TRANSLATE_1_1 or + vtr == L2_VTR_OP.L2_TRANSLATE_2_1): + self._tag1 = tag + self._push1q = push1q + if (vtr == L2_VTR_OP.L2_PUSH_2 or + vtr == L2_VTR_OP.L2_TRANSLATE_1_2 or + vtr == L2_VTR_OP.L2_TRANSLATE_2_2): + self._tag1 = outer + self._tag2 = inner + self._push1q = push1q + + self.test.vapi.sw_interface_set_l2_tag_rewrite( + self.sw_if_index, vtr, push=self._push1q, + tag1=self._tag1, tag2=self._tag2) + self._vtr = vtr + + +class VppDot1QSubint(VppSubInterface): + + @property + def vlan(self): + """VLAN tag""" + return self._vlan + + def __init__(self, test, parent, sub_id, vlan=None): + if vlan is None: + vlan = sub_id + self._vlan = vlan + r = test.vapi.create_vlan_subif(parent.sw_if_index, vlan) + self._sw_if_index = r.sw_if_index + super(VppDot1QSubint, self).__init__(test, parent, sub_id) + + def create_arp_req(self): + packet = VppPGInterface.create_arp_req(self) + return self.add_dot1_layer(packet) + + def create_ndp_req(self): + packet = VppPGInterface.create_ndp_req(self) + return self.add_dot1_layer(packet) + + # called before sending packet + def add_dot1_layer(self, packet): + return self.add_dot1q_layer(packet, self.vlan) + + # called on received packet to "reverse" the add call + def remove_dot1_layer(self, packet): + return self.remove_dot1q_layer(packet, self.vlan) + + +class VppDot1ADSubint(VppSubInterface): + + @property + def outer_vlan(self): + """Outer VLAN tag""" + return self._outer_vlan + + @property + def inner_vlan(self): + """Inner VLAN tag""" + return self._inner_vlan + + def __init__(self, test, parent, sub_id, outer_vlan, inner_vlan): + r = test.vapi.create_subif(parent.sw_if_index, sub_id, outer_vlan, + inner_vlan, dot1ad=1, two_tags=1, + exact_match=1) + self._sw_if_index = r.sw_if_index + self._outer_vlan = outer_vlan + self._inner_vlan = inner_vlan + super(VppDot1ADSubint, self).__init__(test, parent, sub_id) + + def create_arp_req(self): + packet = VppPGInterface.create_arp_req(self) + return self.add_dot1_layer(packet) + + def create_ndp_req(self): + packet = VppPGInterface.create_ndp_req(self) + return self.add_dot1_layer(packet) + + def add_dot1_layer(self, packet): + return self.add_dot1ad_layer(packet, self.outer_vlan, self.inner_vlan) + + def remove_dot1_layer(self, packet): + return self.remove_dot1ad_layer(packet, self.outer_vlan, + self.inner_vlan) + + +class VppP2PSubint(VppSubInterface): + + def __init__(self, test, parent, sub_id, remote_mac): + r = test.vapi.create_p2pethernet_subif(parent.sw_if_index, + remote_mac, sub_id) + self._sw_if_index = r.sw_if_index + super(VppP2PSubint, self).__init__(test, parent, sub_id) + + def add_dot1_layer(self, packet): + return packet + + def remove_dot1_layer(self, packet): + return packet + + def create_arp_req(self): + packet = VppPGInterface.create_arp_req(self) + return packet + + def create_ndp_req(self): + packet = VppPGInterface.create_ndp_req(self) + return packet |