aboutsummaryrefslogtreecommitdiffstats
path: root/extras/hs-test
diff options
context:
space:
mode:
Diffstat (limited to 'extras/hs-test')
-rw-r--r--extras/hs-test/Makefile174
-rw-r--r--extras/hs-test/README.rst319
-rw-r--r--extras/hs-test/address_allocator.go98
-rw-r--r--extras/hs-test/container.go383
-rw-r--r--extras/hs-test/cpu.go100
-rw-r--r--extras/hs-test/docker/Dockerfile.build8
-rw-r--r--extras/hs-test/docker/Dockerfile.curl7
-rw-r--r--extras/hs-test/docker/Dockerfile.nginx20
-rw-r--r--extras/hs-test/docker/Dockerfile.nginx-http324
-rw-r--r--extras/hs-test/docker/Dockerfile.nginx-server12
-rw-r--r--extras/hs-test/docker/Dockerfile.vpp33
-rw-r--r--extras/hs-test/echo_test.go51
-rw-r--r--extras/hs-test/framework_test.go22
-rw-r--r--extras/hs-test/go.mod33
-rw-r--r--extras/hs-test/go.sum70
-rw-r--r--extras/hs-test/hst_suite.go502
-rw-r--r--extras/hs-test/http_test.go295
-rw-r--r--extras/hs-test/ldp_test.go84
-rw-r--r--extras/hs-test/linux_iperf_test.go40
-rw-r--r--extras/hs-test/mirroring_test.go27
-rw-r--r--extras/hs-test/netconfig.go383
-rw-r--r--extras/hs-test/proxy_test.go115
-rw-r--r--extras/hs-test/raw_session_test.go40
-rw-r--r--extras/hs-test/resources/cert/localhost.crt21
-rw-r--r--extras/hs-test/resources/cert/localhost.key28
-rw-r--r--extras/hs-test/resources/envoy/envoy.log0
-rw-r--r--extras/hs-test/resources/envoy/proxy.yaml53
-rw-r--r--extras/hs-test/resources/envoy/vcl.conf7
-rw-r--r--extras/hs-test/resources/nginx/html/index.html6
-rw-r--r--extras/hs-test/resources/nginx/nginx.conf30
-rw-r--r--extras/hs-test/resources/nginx/nginx_http3.conf26
-rw-r--r--extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf84
-rw-r--r--extras/hs-test/resources/nginx/nginx_server_mirroring.conf32
-rw-r--r--extras/hs-test/resources/nginx/vcl.conf11
-rwxr-xr-xextras/hs-test/script/build_boringssl.sh4
-rwxr-xr-xextras/hs-test/script/build_curl.sh5
-rwxr-xr-xextras/hs-test/script/build_hst.sh81
-rwxr-xr-xextras/hs-test/script/build_nginx.sh5
-rw-r--r--extras/hs-test/script/compress.sh33
-rwxr-xr-xextras/hs-test/script/nginx_ldp.sh3
-rw-r--r--extras/hs-test/suite_nginx_test.go132
-rw-r--r--extras/hs-test/suite_no_topo_test.go108
-rw-r--r--extras/hs-test/suite_ns_test.go117
-rw-r--r--extras/hs-test/suite_tap_test.go84
-rw-r--r--extras/hs-test/suite_veth_test.go142
-rw-r--r--extras/hs-test/test94
-rw-r--r--extras/hs-test/tools/http_server/http_server.go26
-rw-r--r--extras/hs-test/topo-containers/2peerVeth.yaml25
-rw-r--r--extras/hs-test/topo-containers/nginxProxyAndServer.yaml20
-rw-r--r--extras/hs-test/topo-containers/ns.yaml27
-rw-r--r--extras/hs-test/topo-containers/single.yaml47
-rw-r--r--extras/hs-test/topo-network/2peerVeth.yaml25
-rw-r--r--extras/hs-test/topo-network/2taps.yaml18
-rw-r--r--extras/hs-test/topo-network/ns.yaml23
-rw-r--r--extras/hs-test/topo-network/tap.yaml10
-rw-r--r--extras/hs-test/topo.go25
-rw-r--r--extras/hs-test/utils.go96
-rw-r--r--extras/hs-test/vars7
-rw-r--r--extras/hs-test/vcl_test.go156
-rw-r--r--extras/hs-test/vppinstance.go476
60 files changed, 4927 insertions, 0 deletions
diff --git a/extras/hs-test/Makefile b/extras/hs-test/Makefile
new file mode 100644
index 00000000000..b809b170e00
--- /dev/null
+++ b/extras/hs-test/Makefile
@@ -0,0 +1,174 @@
+export HS_ROOT=$(CURDIR)
+
+ifeq ($(VERBOSE),)
+VERBOSE=false
+endif
+
+ifeq ($(PERSIST),)
+PERSIST=false
+endif
+
+ifeq ($(UNCONFIGURE),)
+UNCONFIGURE=false
+endif
+
+ifeq ($(TEST),)
+TEST=all
+endif
+
+ifeq ($(TEST-HS),)
+TEST-HS=all
+endif
+
+ifeq ($(DEBUG),)
+DEBUG=false
+endif
+
+ifeq ($(CPUS),)
+CPUS=1
+endif
+
+ifeq ($(PARALLEL),)
+PARALLEL=1
+endif
+
+ifeq ($(REPEAT),)
+REPEAT=0
+endif
+
+ifeq ($(VPPSRC),)
+VPPSRC=$(shell pwd)/../..
+endif
+
+ifeq ($(UBUNTU_CODENAME),)
+UBUNTU_CODENAME=$(shell grep '^UBUNTU_CODENAME=' /etc/os-release | cut -f2- -d=)
+endif
+
+ifeq ($(ARCH),)
+ARCH=$(shell dpkg --print-architecture)
+endif
+
+list_tests = @go run github.com/onsi/ginkgo/v2/ginkgo --dry-run -v --no-color --seed=2 | head -n -1 | grep 'Test' | \
+ sed 's/^/* /; s/\(Suite\) /\1\//g'
+
+.PHONY: help
+help:
+ @echo "Make targets:"
+ @echo " test - run tests"
+ @echo " test-debug - run tests (vpp debug image)"
+ @echo " build - build test infra"
+ @echo " build-cov - coverage build of VPP and Docker images"
+ @echo " build-debug - build test infra (vpp debug image)"
+ @echo " build-go - just build golang files"
+ @echo " fixstyle - format .go source files"
+ @echo " list-tests - list all tests"
+ @echo
+ @echo "make build arguments:"
+ @echo " UBUNTU_VERSION - ubuntu version for docker image"
+ @echo " HST_EXTENDED_TESTS - build extended tests"
+ @echo
+ @echo "make test arguments:"
+ @echo " PERSIST=[true|false] - whether clean up topology and dockers after test"
+ @echo " VERBOSE=[true|false] - verbose output"
+ @echo " UNCONFIGURE=[true|false] - unconfigure selected test"
+ @echo " DEBUG=[true|false] - attach VPP to GDB"
+ @echo " TEST=[test-name] - specific test to run"
+ @echo " CPUS=[n-cpus] - number of cpus to allocate to VPP and containers"
+ @echo " VPPSRC=[path-to-vpp-src] - path to vpp source files (for gdb)"
+ @echo " PARALLEL=[n-cpus] - number of test processes to spawn to run in parallel"
+ @echo " REPEAT=[n] - repeat tests up to N times or until a failure occurs"
+ @echo
+ @echo "List of all tests:"
+ $(call list_tests)
+
+.PHONY: list-tests
+list-tests:
+ $(call list_tests)
+
+.PHONY: build-vpp-release
+build-vpp-release:
+ @make -C ../.. build-release
+
+.PHONY: build-vpp-debug
+build-vpp-debug:
+ @make -C ../.. build
+
+.PHONY: build-vpp-gcov
+build-vpp-gcov:
+ @make -C ../.. build-vpp-gcov
+
+.build.ok: build
+ @touch .build.ok
+
+.build_debug.ok: build-debug
+ @touch .build.ok
+
+.PHONY: test
+test: .deps.ok .build.ok
+ # '-' ignores the exit status, it is set in compress.sh
+ # necessary so gmake won't skip executing the bash script
+ -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \
+ --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST) --cpus=$(CPUS) \
+ --vppsrc=$(VPPSRC) --parallel=$(PARALLEL) --repeat=$(REPEAT)
+ @bash ./script/compress.sh
+
+.PHONY: test-debug
+test-debug: .deps.ok .build_debug.ok
+ # '-' ignores the exit status, it is set in compress.sh
+ # necessary so gmake won't skip executing the bash script
+ -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \
+ --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST) --cpus=$(CPUS) \
+ --vppsrc=$(VPPSRC) --parallel=$(PARALLEL) --repeat=$(REPEAT)
+ @bash ./script/compress.sh
+
+.PHONY: test-cov
+test-cov: .deps.ok .build.ok
+ -bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \
+ --unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST-HS) --cpus=$(CPUS) \
+ --vppsrc=$(VPPSRC)
+ @make -C ../.. test-cov-post HS_TEST=1
+ @bash ./script/compress.sh
+
+.PHONY: build-go
+build-go:
+ go build ./tools/http_server
+
+.PHONY: build
+build: .deps.ok build-vpp-release build-go
+ @rm -f .build.ok
+ bash ./script/build_hst.sh release
+ @touch .build.ok
+
+.PHONY: build-cov
+build-cov: .deps.ok build-vpp-gcov build-go
+ @rm -f .build.vpp
+ bash ./script/build_hst.sh gcov
+ @touch .build.vpp
+
+.PHONY: build-debug
+build-debug: .deps.ok build-vpp-debug build-go
+ @rm -f .build.ok
+ bash ./script/build_hst.sh debug
+ @touch .build.ok
+
+.deps.ok:
+ @sudo make install-deps
+
+.PHONY: install-deps
+install-deps:
+ @rm -f .deps.ok
+ @apt-get update \
+ && apt-get install -y apt-transport-https ca-certificates curl software-properties-common \
+ apache2-utils wrk bridge-utils
+ @if [ ! -f /usr/share/keyrings/docker-archive-keyring.gpg ] ; then \
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \
+ echo "deb [arch=$(ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(UBUNTU_CODENAME) stable" \
+ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null ; \
+ apt-get update; \
+ fi
+ @touch .deps.ok
+
+.PHONY: fixstyle
+fixstyle:
+ @gofmt -w .
+ @go mod tidy
diff --git a/extras/hs-test/README.rst b/extras/hs-test/README.rst
new file mode 100644
index 00000000000..1dc1039b33f
--- /dev/null
+++ b/extras/hs-test/README.rst
@@ -0,0 +1,319 @@
+Host stack test framework
+=========================
+
+Overview
+--------
+
+The goal of the Host stack test framework (**hs-test**) is to ease writing and running end-to-end tests for VPP's Host Stack.
+End-to-end tests often want multiple VPP instances, network namespaces, different types of interfaces
+and to execute external tools or commands. With such requirements the existing VPP test framework is not sufficient.
+For this, ``Go`` was chosen as a high level language, allowing rapid development, with ``Docker`` and ``ip`` being the tools for creating required topology.
+
+`Ginkgo`_ forms the base framework upon which the *hs-test* is built and run.
+All tests are technically in a single suite because we are only using ``package main``. We simulate suite behavior by grouping tests by the topology they require.
+This allows us to run those mentioned groups in parallel, but not individual tests in parallel.
+
+
+Anatomy of a test case
+----------------------
+
+**Prerequisites**:
+
+* Install hs-test dependencies with ``make install-deps``
+* Tests use *hs-test*'s own docker image, so building it before starting tests is a prerequisite. Run ``make build[-debug]`` to do so
+* Docker has to be installed and Go has to be in path of both the running user and root
+* Root privileges are required to run tests as it uses Linux ``ip`` command for configuring topology
+
+**Action flow when running a test case**:
+
+#. It starts with running ``make test``. Optional arguments are VERBOSE, PERSIST (topology configuration isn't cleaned up after test run),
+ TEST=<test-name> to run a specific test and PARALLEL=[n-cpus].
+#. ``make list-tests`` (or ``make help``) shows all tests. The current `list of tests`_ is at the bottom of this document.
+#. ``Ginkgo`` looks for a spec suite in the current directory and then compiles it to a .test binary
+#. The Ginkgo test framework runs each function that was registered manually using ``registerMySuiteTest(s *MySuite)``. Each of these functions correspond to a suite
+#. Ginkgo's ``RunSpecs(t, "Suite description")`` function is the entry point and does the following:
+
+ #. Ginkgo compiles the spec, builds a spec tree
+ #. ``Describe`` container nodes in suite\_\*_test.go files are run (in series by default, or in parallel with the argument PARALLEL=[n-cpus])
+ #. Suite is initialized. The topology is loaded and configured in this step
+ #. Registered tests are run in generated ``It`` subject nodes
+ #. Execute tear-down functions, which currently consists of stopping running containers
+ and clean-up of test topology
+
+Adding a test case
+------------------
+
+This describes adding a new test case to an existing suite.
+For adding a new suite, please see `Modifying the framework`_ below.
+
+#. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists
+#. Declare method whose name ends with ``Test`` and specifies its parameter as a pointer to the suite's struct (defined in ``suite_*_test.go``)
+#. Implement test behaviour inside the test method. This typically includes the following:
+
+ #. Retrieve a running container in which to run some action. Method ``getContainerByName``
+ from ``HstSuite`` struct serves this purpose
+ #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``vppctl`` method to access debug CLI
+ #. Run arbitrary commands inside the containers with ``exec`` method
+ #. Run other external tool with one of the preexisting functions in the ``utils.go`` file.
+ For example, use ``wget`` with ``startWget`` function
+ #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else
+ #. Verify results of your tests using ``assert`` methods provided by the test suite, implemented by HstSuite struct or use ``Gomega`` assert functions.
+
+#. Create an ``init()`` function and register the test using ``register*SuiteTests(testCaseFunction)``
+
+
+**Example test case**
+
+Assumed are two docker containers, each with its own VPP instance running. One VPP then pings the other.
+This can be put in file ``extras/hs-test/my_test.go`` and run with command ``make test TEST=MyTest`` or ``ginkgo -v --trace --focus MyTest``.
+
+::
+
+ package main
+
+ import (
+ "fmt"
+ )
+
+ func init(){
+ registerMySuiteTest(MyTest)
+ }
+
+ func MyTest(s *MySuite) {
+ clientVpp := s.getContainerByName("client-vpp").vppInstance
+
+ serverVethAddress := s.netInterfaces["server-iface"].AddressString()
+
+ result := clientVpp.vppctl("ping " + serverVethAddress)
+ s.assertNotNil(result)
+ s.log(result)
+ }
+
+Modifying the framework
+-----------------------
+
+**Adding a test suite**
+
+.. _test-convention:
+
+#. To add a new suite, create a new file. Naming convention for the suite files is ``suite_name_test.go`` where *name* will be replaced
+ by the actual name
+
+#. Make a ``struct``, in the suite file, with at least ``HstSuite`` struct as its member.
+ HstSuite provides functionality that can be shared for all suites, like starting containers
+
+ ::
+
+ type MySuite struct {
+ HstSuite
+ }
+
+#. Create a new slice that will contain test functions with a pointer to the suite's struct: ``var myTests = []func(s *MySuite){}``
+
+#. Then create a new function that will append test functions to that slice:
+
+ ::
+
+ func registerMySuiteTests(tests ...func(s *MySuite)) {
+ nginxTests = append(myTests, tests...)
+ }
+
+#. In suite file, implement ``SetupSuite`` method which Ginkgo runs once before starting any of the tests.
+ It's important here to call ``configureNetworkTopology`` method,
+ pass the topology name to the function in a form of file name of one of the *yaml* files in ``topo-network`` folder.
+ Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo-network/myTopology.yaml``
+ This will ensure network topology, such as network interfaces and namespaces, will be created.
+ Another important method to call is ``loadContainerTopology()`` which will load
+ containers and shared volumes used by the suite. This time the name passed to method corresponds
+ to file in ``extras/hs-test/topo-containers`` folder
+
+ ::
+
+ func (s *MySuite) SetupSuite() {
+ s.HstSuite.SetupSuite()
+
+ // Add custom setup code here
+
+ s.configureNetworkTopology("myTopology")
+ s.loadContainerTopology("2peerVeth")
+ }
+
+#. In suite file, implement ``SetupTest`` method which gets executed before each test. Starting containers and
+ configuring VPP is usually placed here
+
+ ::
+
+ func (s *MySuite) SetupTest() {
+ s.HstSuite.setupTest()
+ s.SetupVolumes()
+ s.SetupContainers()
+ }
+
+#. In order for ``Ginkgo`` to run this suite, we need to create a ``Describe`` container node with setup nodes and an ``It`` subject node.
+ Place them at the end of the suite file
+
+ * Declare a suite struct variable before anything else
+ * To use ``BeforeAll()`` and ``AfterAll()``, the container has to be marked as ``Ordered``
+ * Because the container is now marked as Ordered, if a test fails, all the subsequent tests are skipped.
+ To override this behavior, decorate the container node with ``ContinueOnFailure``
+
+ ::
+
+ var _ = Describe("MySuite", Ordered, ContinueOnFailure, func() {
+ var s MySuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+ for _, test := range mySuiteTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+ test(&s)
+ }, SpecTimeout(time.Minute*5))
+ }
+ })
+
+#. Notice the loop - it will generate multiple ``It`` nodes, each running a different test.
+ ``test := test`` is necessary, otherwise only the last test in a suite will run.
+ For a more detailed description, check Ginkgo's documentation: https://onsi.github.io/ginkgo/#dynamically-generating-specs\.
+
+#. ``funcValue.Name()`` returns the full name of a function (e.g. ``fd.io/hs-test.MyTest``), however, we only need the test name (``MyTest``).
+
+#. To run certain tests solo, create a new slice that will only contain tests that have to run solo and a new register function.
+ Add a ``Serial`` decorator to the container node and ``Label("SOLO")`` to the ``It`` subject node:
+
+ ::
+
+ var _ = Describe("MySuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ ...
+ It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+ test(&s)
+ }, SpecTimeout(time.Minute*5))
+ })
+
+#. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above
+
+**Adding a topology element**
+
+Topology configuration exists as ``yaml`` files in the ``extras/hs-test/topo-network`` and
+``extras/hs-test/topo-containers`` folders. Processing of a network topology file for a particular test suite
+is started by the ``configureNetworkTopology`` method depending on which file's name is passed to it.
+Specified file is loaded and converted into internal data structures which represent various elements of the topology.
+After parsing the configuration, framework loops over the elements and configures them one by one on the host system.
+
+These are currently supported types of network elements.
+
+* ``netns`` - network namespace
+* ``veth`` - veth network interface, optionally with target network namespace or IPv4 address
+* ``bridge`` - ethernet bridge to connect created interfaces, optionally with target network namespace
+* ``tap`` - tap network interface with IP address
+
+Similarly, container topology is started by ``loadContainerTopology()``, configuration file is processed
+so that test suite retains map of defined containers and uses that to start them at the beginning
+of each test case and stop containers after the test finishes. Container configuration can specify
+also volumes which allow to share data between containers or between host system and containers.
+
+Supporting a new type of topology element requires adding code to recognize the new element type during loading.
+And adding code to set up the element in the host system with some Linux tool, such as *ip*.
+This should be implemented in ``netconfig.go`` for network and in ``container.go`` for containers and volumes.
+
+**Communicating between containers**
+
+When two VPP instances or other applications, each in its own Docker container,
+want to communicate there are typically two ways this can be done within *hs-test*.
+
+* Network interfaces. Containers are being created with ``-d --network host`` options,
+ so they are connected with interfaces created in host system
+* Shared folders. Containers are being created with ``-v`` option to create shared `volumes`_ between host system and containers
+ or just between containers
+
+Host system connects to VPP instances running in containers using a shared folder
+where binary API socket is accessible by both sides.
+
+**Adding an external tool**
+
+If an external program should be executed as part of a test case, it might be useful to wrap its execution in its own function.
+These types of functions are placed in the ``utils.go`` file. If the external program is not available by default in Docker image,
+add its installation to ``extras/hs-test/Dockerfile.vpp`` in ``apt-get install`` command.
+Alternatively copy the executable from host system to the Docker image, similarly how the VPP executables and libraries are being copied.
+
+**Skipping tests**
+
+``HstSuite`` provides several methods that can be called in tests for skipping it conditionally or unconditionally such as:
+``skip()``, ``SkipIfMultiWorker()``, ``SkipUnlessExtendedTestsBuilt()``. You can also use Ginkgo's ``Skip()``.
+However the tests currently run under test suites which set up topology and containers before actual test is run. For the reason of saving
+test run time it is not advisable to use aforementioned skip methods and instead, just don't register the test.
+
+**Debugging a test**
+
+It is possible to debug VPP by attaching ``gdb`` before test execution by adding ``DEBUG=true`` like follows:
+
+::
+
+ $ make test TEST=LDPreloadIperfVppTest DEBUG=true
+ ...
+ run following command in different terminal:
+ docker exec -it server-vpp2456109 gdb -ex "attach $(docker exec server-vpp2456109 pidof vpp)"
+ Afterwards press CTRL+\ to continue
+
+If a test consists of more VPP instances then this is done for each of them.
+
+
+**Eternal dependencies**
+
+* Linux tools ``ip``, ``brctl``
+* Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made,
+ they are reasonably up-to-date automatically
+* Programs in Docker images - ``envoyproxy/envoy-contrib`` and ``nginx``
+* ``http_server`` - homegrown application that listens on specified port and sends a test file in response
+* Non-standard Go libraries - see ``extras/hs-test/go.mod``
+
+Generally, these will be updated on a per-need basis, for example when a bug is discovered
+or a new version incompatibility issue occurs.
+
+
+.. _ginkgo: https://onsi.github.io/ginkgo/
+.. _volumes: https://docs.docker.com/storage/volumes/
+
+**List of tests**
+
+.. _list of tests:
+
+Please update this list whenever you add a new test by pasting the output below.
+
+* NsSuite/HttpTpsTest
+* NsSuite/VppProxyHttpTcpTest
+* NsSuite/VppProxyHttpTlsTest
+* NsSuite/EnvoyProxyHttpTcpTest
+* NginxSuite/MirroringTest
+* VethsSuiteSolo TcpWithLossTest [SOLO]
+* NoTopoSuiteSolo HttpStaticPromTest [SOLO]
+* TapSuite/LinuxIperfTest
+* NoTopoSuite/NginxHttp3Test
+* NoTopoSuite/NginxAsServerTest
+* NoTopoSuite/NginxPerfCpsTest
+* NoTopoSuite/NginxPerfRpsTest
+* NoTopoSuite/NginxPerfWrkTest
+* VethsSuite/EchoBuiltinTest
+* VethsSuite/HttpCliTest
+* VethsSuite/LDPreloadIperfVppTest
+* VethsSuite/VppEchoQuicTest
+* VethsSuite/VppEchoTcpTest
+* VethsSuite/VppEchoUdpTest
+* VethsSuite/XEchoVclClientUdpTest
+* VethsSuite/XEchoVclClientTcpTest
+* VethsSuite/XEchoVclServerUdpTest
+* VethsSuite/XEchoVclServerTcpTest
+* VethsSuite/VclEchoTcpTest
+* VethsSuite/VclEchoUdpTest
+* VethsSuite/VclRetryAttachTest
diff --git a/extras/hs-test/address_allocator.go b/extras/hs-test/address_allocator.go
new file mode 100644
index 00000000000..e05ea76b9bb
--- /dev/null
+++ b/extras/hs-test/address_allocator.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/edwarnicke/exechelper"
+)
+
+type AddressCounter = int
+
+type Ip4AddressAllocator struct {
+ networks map[int]AddressCounter
+ chosenOctet int
+ assignedIps []string
+}
+
+func (a *Ip4AddressAllocator) AddNetwork(networkNumber int) {
+ a.networks[networkNumber] = 1
+}
+
+func (a *Ip4AddressAllocator) NewIp4InterfaceAddress(inputNetworkNumber ...int) (string, error) {
+ var networkNumber int = 0
+ if len(inputNetworkNumber) > 0 {
+ networkNumber = inputNetworkNumber[0]
+ }
+
+ if _, ok := a.networks[networkNumber]; !ok {
+ a.AddNetwork(networkNumber)
+ }
+
+ numberOfAddresses := a.networks[networkNumber]
+
+ if numberOfAddresses == 254 {
+ return "", fmt.Errorf("no available IPv4 addresses")
+ }
+
+ address, err := a.createIpAddress(networkNumber, numberOfAddresses)
+
+ a.networks[networkNumber] = numberOfAddresses + 1
+
+ return address + "/24", err
+}
+
+// Creates a file every time an IP is assigned: used to keep track of addresses in use.
+// If an address is not in use, 'counter' is then copied to 'chosenOctet' and it is used for the remaining tests.
+// Also checks host IP addresses.
+func (a *Ip4AddressAllocator) createIpAddress(networkNumber int, numberOfAddresses int) (string, error) {
+ hostIps, _ := exechelper.CombinedOutput("ip a")
+ counter := 10
+ var address string
+
+ for {
+ if a.chosenOctet != 0 {
+ address = fmt.Sprintf("10.%v.%v.%v", a.chosenOctet, networkNumber, numberOfAddresses)
+ file, err := os.Create(address)
+ if err != nil {
+ return "", errors.New("unable to create file: " + fmt.Sprint(err))
+ }
+ file.Close()
+ break
+ } else {
+ address = fmt.Sprintf("10.%v.%v.%v", counter, networkNumber, numberOfAddresses)
+ _, err := os.Stat(address)
+ if err == nil || strings.Contains(string(hostIps), address) {
+ counter++
+ } else if os.IsNotExist(err) {
+ file, err := os.Create(address)
+ if err != nil {
+ return "", errors.New("unable to create file: " + fmt.Sprint(err))
+ }
+ file.Close()
+ a.chosenOctet = counter
+ break
+ } else {
+ return "", errors.New("an error occurred while checking if a file exists: " + fmt.Sprint(err))
+ }
+ }
+ }
+
+ a.assignedIps = append(a.assignedIps, address)
+ return address, nil
+}
+
+func (a *Ip4AddressAllocator) deleteIpAddresses() {
+ for ip := range a.assignedIps {
+ os.Remove(a.assignedIps[ip])
+ }
+}
+
+func NewIp4AddressAllocator() *Ip4AddressAllocator {
+ var ip4AddrAllocator = new(Ip4AddressAllocator)
+ ip4AddrAllocator.networks = make(map[int]AddressCounter)
+ ip4AddrAllocator.AddNetwork(0)
+ return ip4AddrAllocator
+}
diff --git a/extras/hs-test/container.go b/extras/hs-test/container.go
new file mode 100644
index 00000000000..44d84d5fe65
--- /dev/null
+++ b/extras/hs-test/container.go
@@ -0,0 +1,383 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "text/template"
+ "time"
+
+ "github.com/edwarnicke/exechelper"
+ . "github.com/onsi/ginkgo/v2"
+)
+
+const (
+ logDir string = "/tmp/hs-test/"
+ volumeDir string = "/volumes"
+)
+
+var (
+ workDir, _ = os.Getwd()
+)
+
+type Volume struct {
+ hostDir string
+ containerDir string
+ isDefaultWorkDir bool
+}
+
+type Container struct {
+ suite *HstSuite
+ isOptional bool
+ runDetached bool
+ name string
+ image string
+ extraRunningArgs string
+ volumes map[string]Volume
+ envVars map[string]string
+ vppInstance *VppInstance
+ allocatedCpus []int
+}
+
+func newContainer(suite *HstSuite, yamlInput ContainerConfig) (*Container, error) {
+ containerName := yamlInput["name"].(string)
+ if len(containerName) == 0 {
+ err := fmt.Errorf("container name must not be blank")
+ return nil, err
+ }
+
+ var container = new(Container)
+ container.volumes = make(map[string]Volume)
+ container.envVars = make(map[string]string)
+ container.name = containerName
+ container.suite = suite
+
+ if image, ok := yamlInput["image"]; ok {
+ container.image = image.(string)
+ } else {
+ container.image = "hs-test/vpp"
+ }
+
+ if args, ok := yamlInput["extra-args"]; ok {
+ container.extraRunningArgs = args.(string)
+ } else {
+ container.extraRunningArgs = ""
+ }
+
+ if isOptional, ok := yamlInput["is-optional"]; ok {
+ container.isOptional = isOptional.(bool)
+ } else {
+ container.isOptional = false
+ }
+
+ if runDetached, ok := yamlInput["run-detached"]; ok {
+ container.runDetached = runDetached.(bool)
+ } else {
+ container.runDetached = true
+ }
+
+ if _, ok := yamlInput["volumes"]; ok {
+ workingVolumeDir := logDir + CurrentSpecReport().LeafNodeText + volumeDir
+ workDirReplacer := strings.NewReplacer("$HST_DIR", workDir)
+ volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
+ for _, volu := range yamlInput["volumes"].([]interface{}) {
+ volumeMap := volu.(ContainerConfig)
+ hostDir := workDirReplacer.Replace(volumeMap["host-dir"].(string))
+ hostDir = volDirReplacer.Replace(hostDir)
+ containerDir := volumeMap["container-dir"].(string)
+ isDefaultWorkDir := false
+
+ if isDefault, ok := volumeMap["is-default-work-dir"]; ok {
+ isDefaultWorkDir = isDefault.(bool)
+ }
+ container.addVolume(hostDir, containerDir, isDefaultWorkDir)
+ }
+ }
+
+ if _, ok := yamlInput["vars"]; ok {
+ for _, envVar := range yamlInput["vars"].([]interface{}) {
+ envVarMap := envVar.(ContainerConfig)
+ name := envVarMap["name"].(string)
+ value := envVarMap["value"].(string)
+ container.addEnvVar(name, value)
+ }
+ }
+ return container, nil
+}
+
+func (c *Container) getWorkDirVolume() (res Volume, exists bool) {
+ for _, v := range c.volumes {
+ if v.isDefaultWorkDir {
+ res = v
+ exists = true
+ return
+ }
+ }
+ return
+}
+
+func (c *Container) getHostWorkDir() (res string) {
+ if v, ok := c.getWorkDirVolume(); ok {
+ res = v.hostDir
+ }
+ return
+}
+
+func (c *Container) getContainerWorkDir() (res string) {
+ if v, ok := c.getWorkDirVolume(); ok {
+ res = v.containerDir
+ }
+ return
+}
+
+func (c *Container) getContainerArguments() string {
+ args := "--ulimit nofile=90000:90000 --cap-add=all --privileged --network host --rm"
+ args += c.getVolumesAsCliOption()
+ args += c.getEnvVarsAsCliOption()
+ if *vppSourceFileDir != "" {
+ args += fmt.Sprintf(" -v %s:%s", *vppSourceFileDir, *vppSourceFileDir)
+ }
+ args += " --name " + c.name + " " + c.image
+ args += " " + c.extraRunningArgs
+ return args
+}
+
+func (c *Container) runWithRetry(cmd string) error {
+ nTries := 5
+ for i := 0; i < nTries; i++ {
+ err := exechelper.Run(cmd)
+ if err == nil {
+ return nil
+ }
+ time.Sleep(1 * time.Second)
+ }
+ return fmt.Errorf("failed to run container command")
+}
+
+func (c *Container) create() error {
+ cmd := "docker create " + c.getContainerArguments()
+ c.suite.log(cmd)
+ return exechelper.Run(cmd)
+}
+
+func (c *Container) allocateCpus() {
+ c.suite.startedContainers = append(c.suite.startedContainers, c)
+ c.allocatedCpus = c.suite.AllocateCpus()
+ c.suite.log("Allocated CPUs " + fmt.Sprint(c.allocatedCpus) + " to container " + c.name)
+}
+
+func (c *Container) start() error {
+ cmd := "docker start " + c.name
+ c.suite.log(cmd)
+ return c.runWithRetry(cmd)
+}
+
+func (c *Container) prepareCommand() (string, error) {
+ if c.name == "" {
+ return "", fmt.Errorf("run container failed: name is blank")
+ }
+
+ cmd := "docker run "
+ if c.runDetached {
+ cmd += " -d"
+ }
+
+ c.allocateCpus()
+ cmd += fmt.Sprintf(" --cpuset-cpus=\"%d-%d\"", c.allocatedCpus[0], c.allocatedCpus[len(c.allocatedCpus)-1])
+ cmd += " " + c.getContainerArguments()
+
+ c.suite.log(cmd)
+ return cmd, nil
+}
+
+func (c *Container) combinedOutput() (string, error) {
+ cmd, err := c.prepareCommand()
+ if err != nil {
+ return "", err
+ }
+
+ byteOutput, err := exechelper.CombinedOutput(cmd)
+ return string(byteOutput), err
+}
+
+func (c *Container) run() error {
+ cmd, err := c.prepareCommand()
+ if err != nil {
+ return err
+ }
+ return c.runWithRetry(cmd)
+}
+
+func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) {
+ var volume Volume
+ volume.hostDir = hostDir
+ volume.containerDir = containerDir
+ volume.isDefaultWorkDir = isDefaultWorkDir
+ c.volumes[hostDir] = volume
+}
+
+func (c *Container) getVolumesAsCliOption() string {
+ cliOption := ""
+
+ if len(c.volumes) > 0 {
+ for _, volume := range c.volumes {
+ cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir)
+ }
+ }
+
+ return cliOption
+}
+
+func (c *Container) addEnvVar(name string, value string) {
+ c.envVars[name] = value
+}
+
+func (c *Container) getEnvVarsAsCliOption() string {
+ cliOption := ""
+ if len(c.envVars) == 0 {
+ return cliOption
+ }
+
+ for name, value := range c.envVars {
+ cliOption += fmt.Sprintf(" -e %s=%s", name, value)
+ }
+ return cliOption
+}
+
+func (c *Container) newVppInstance(cpus []int, additionalConfigs ...Stanza) (*VppInstance, error) {
+ vpp := new(VppInstance)
+ vpp.container = c
+ vpp.cpus = cpus
+ vpp.additionalConfig = append(vpp.additionalConfig, additionalConfigs...)
+ c.vppInstance = vpp
+ return vpp, nil
+}
+
+func (c *Container) copy(sourceFileName string, targetFileName string) error {
+ cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
+ return cmd.Run()
+}
+
+func (c *Container) createFile(destFileName string, content string) error {
+ f, err := os.CreateTemp("/tmp", "hst-config"+c.suite.pid)
+ if err != nil {
+ return err
+ }
+ defer os.Remove(f.Name())
+
+ if _, err := f.Write([]byte(content)); err != nil {
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ c.copy(f.Name(), destFileName)
+ return nil
+}
+
+/*
+ * Executes in detached mode so that the started application can continue to run
+ * without blocking execution of test
+ */
+func (c *Container) execServer(command string, arguments ...any) {
+ serverCommand := fmt.Sprintf(command, arguments...)
+ containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
+ " " + c.name + " " + serverCommand
+ GinkgoHelper()
+ c.suite.log(containerExecCommand)
+ c.suite.assertNil(exechelper.Run(containerExecCommand))
+}
+
+func (c *Container) exec(command string, arguments ...any) string {
+ cliCommand := fmt.Sprintf(command, arguments...)
+ containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
+ " " + c.name + " " + cliCommand
+ GinkgoHelper()
+ c.suite.log(containerExecCommand)
+ byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
+ c.suite.assertNil(err, fmt.Sprint(err))
+ return string(byteOutput)
+}
+
+func (c *Container) getLogDirPath() string {
+ testId := c.suite.getTestId()
+ testName := CurrentSpecReport().LeafNodeText
+ logDirPath := logDir + testName + "/" + testId + "/"
+
+ cmd := exec.Command("mkdir", "-p", logDirPath)
+ if err := cmd.Run(); err != nil {
+ Fail("mkdir error: " + fmt.Sprint(err))
+ }
+
+ return logDirPath
+}
+
+func (c *Container) saveLogs() {
+ cmd := exec.Command("docker", "inspect", "--format='{{.State.Status}}'", c.name)
+ if output, _ := cmd.CombinedOutput(); !strings.Contains(string(output), "running") {
+ return
+ }
+
+ testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log"
+
+ cmd = exec.Command("docker", "logs", "--details", "-t", c.name)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ Fail("fetching logs error: " + fmt.Sprint(err))
+ }
+
+ f, err := os.Create(testLogFilePath)
+ if err != nil {
+ Fail("file create error: " + fmt.Sprint(err))
+ }
+ fmt.Fprint(f, string(output))
+ f.Close()
+}
+
+// Outputs logs from docker containers. Set 'maxLines' to 0 to output the full log.
+func (c *Container) log(maxLines int) (string, error) {
+ var cmd string
+ if maxLines == 0 {
+ cmd = "docker logs " + c.name
+ } else {
+ cmd = fmt.Sprintf("docker logs --tail %d %s", maxLines, c.name)
+ }
+
+ c.suite.log(cmd)
+ o, err := exechelper.CombinedOutput(cmd)
+ return string(o), err
+}
+
+func (c *Container) stop() error {
+ if c.vppInstance != nil && c.vppInstance.apiStream != nil {
+ c.vppInstance.saveLogs()
+ c.vppInstance.disconnect()
+ }
+ c.vppInstance = nil
+ c.saveLogs()
+ return exechelper.Run("docker stop " + c.name + " -t 0")
+}
+
+func (c *Container) createConfig(targetConfigName string, templateName string, values any) {
+ template := template.Must(template.ParseFiles(templateName))
+
+ f, err := os.CreateTemp(logDir, "hst-config")
+ c.suite.assertNil(err, err)
+ defer os.Remove(f.Name())
+
+ err = template.Execute(f, values)
+ c.suite.assertNil(err, err)
+
+ err = f.Close()
+ c.suite.assertNil(err, err)
+
+ c.copy(f.Name(), targetConfigName)
+}
+
+func init() {
+ cmd := exec.Command("mkdir", "-p", logDir)
+ if err := cmd.Run(); err != nil {
+ panic(err)
+ }
+}
diff --git a/extras/hs-test/cpu.go b/extras/hs-test/cpu.go
new file mode 100644
index 00000000000..49a7dfb02f8
--- /dev/null
+++ b/extras/hs-test/cpu.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ . "github.com/onsi/ginkgo/v2"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+var CgroupPath = "/sys/fs/cgroup/"
+
+type CpuContext struct {
+ cpuAllocator *CpuAllocatorT
+ cpus []int
+}
+
+type CpuAllocatorT struct {
+ cpus []int
+}
+
+var cpuAllocator *CpuAllocatorT = nil
+
+func (c *CpuAllocatorT) Allocate(containerCount int, nCpus int) (*CpuContext, error) {
+ var cpuCtx CpuContext
+
+ // splitting cpus into equal parts; this will over-allocate cores but it's good enough for now
+ maxContainerCount := 4
+ // skip CPU 0
+ minCpu := ((GinkgoParallelProcess() - 1) * maxContainerCount * nCpus) + 1
+ maxCpu := (GinkgoParallelProcess() * maxContainerCount * nCpus)
+
+ if len(c.cpus)-1 < maxCpu {
+ err := fmt.Errorf("could not allocate %d CPUs; available: %d; attempted to allocate cores %d-%d",
+ nCpus*containerCount, len(c.cpus)-1, minCpu, maxCpu)
+ return nil, err
+ }
+ if containerCount == 1 {
+ cpuCtx.cpus = c.cpus[minCpu : minCpu+nCpus]
+ } else if containerCount > 1 && containerCount <= maxContainerCount {
+ cpuCtx.cpus = c.cpus[minCpu+(nCpus*(containerCount-1)) : minCpu+(nCpus*containerCount)]
+ } else {
+ return nil, fmt.Errorf("too many containers; CPU allocation for >%d containers is not implemented", maxContainerCount)
+ }
+
+ cpuCtx.cpuAllocator = c
+ return &cpuCtx, nil
+}
+
+func (c *CpuAllocatorT) readCpus() error {
+ var first, last int
+
+ // Path depends on cgroup version. We need to check which version is in use.
+ // For that following command can be used: 'stat -fc %T /sys/fs/cgroup/'
+ // In case the output states 'cgroup2fs' then cgroups v2 is used, 'tmpfs' in case cgroups v1.
+ cmd := exec.Command("stat", "-fc", "%T", "/sys/fs/cgroup/")
+ byteOutput, err := cmd.CombinedOutput()
+ if err != nil {
+ return err
+ }
+ CpuPath := CgroupPath
+ if strings.Contains(string(byteOutput), "tmpfs") {
+ CpuPath += "cpuset/cpuset.effective_cpus"
+ } else if strings.Contains(string(byteOutput), "cgroup2fs") {
+ CpuPath += "cpuset.cpus.effective"
+ } else {
+ return errors.New("cgroup unknown fs: " + string(byteOutput))
+ }
+
+ file, err := os.Open(CpuPath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ sc := bufio.NewScanner(file)
+ sc.Scan()
+ line := sc.Text()
+ _, err = fmt.Sscanf(line, "%d-%d", &first, &last)
+ if err != nil {
+ return err
+ }
+ for i := first; i <= last; i++ {
+ c.cpus = append(c.cpus, i)
+ }
+ return nil
+}
+
+func CpuAllocator() (*CpuAllocatorT, error) {
+ if cpuAllocator == nil {
+ cpuAllocator = new(CpuAllocatorT)
+ err := cpuAllocator.readCpus()
+ if err != nil {
+ return nil, err
+ }
+ }
+ return cpuAllocator, nil
+}
diff --git a/extras/hs-test/docker/Dockerfile.build b/extras/hs-test/docker/Dockerfile.build
new file mode 100644
index 00000000000..8b2652e93fc
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.build
@@ -0,0 +1,8 @@
+ARG UBUNTU_VERSION
+
+FROM ubuntu:${UBUNTU_VERSION}
+
+RUN apt-get update \
+ && apt-get install -y gcc git make autoconf libtool pkg-config cmake ninja-build golang \
+ && rm -rf /var/lib/apt/lists/*
+
diff --git a/extras/hs-test/docker/Dockerfile.curl b/extras/hs-test/docker/Dockerfile.curl
new file mode 100644
index 00000000000..81d15e86c82
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.curl
@@ -0,0 +1,7 @@
+FROM hs-test/build
+
+COPY script/build_curl.sh /build_curl.sh
+RUN apt-get update && apt-get install wget
+RUN /build_curl.sh
+
+CMD ["/bin/sh"]
diff --git a/extras/hs-test/docker/Dockerfile.nginx b/extras/hs-test/docker/Dockerfile.nginx
new file mode 100644
index 00000000000..11ec6af156d
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.nginx
@@ -0,0 +1,20 @@
+ARG UBUNTU_VERSION
+
+FROM ubuntu:${UBUNTU_VERSION}
+
+RUN apt-get update \
+ && apt-get install -y nginx gdb less \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY vpp-data/lib/* /usr/lib/
+COPY resources/nginx/vcl.conf /vcl.conf
+COPY resources/nginx/nginx.conf /nginx.conf
+COPY script/nginx_ldp.sh /usr/bin/nginx_ldp.sh
+
+ENV VCL_CONFIG=/vcl.conf
+ENV LDP=/usr/lib/libvcl_ldpreload.so
+ENV LDP_DEBUG=0
+ENV VCL_DEBUG=0
+ENV LDP_SID_BIT=8
+
+ENTRYPOINT ["nginx_ldp.sh", "nginx", "-c", "/nginx.conf"]
diff --git a/extras/hs-test/docker/Dockerfile.nginx-http3 b/extras/hs-test/docker/Dockerfile.nginx-http3
new file mode 100644
index 00000000000..5d66a2528a6
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.nginx-http3
@@ -0,0 +1,24 @@
+FROM hs-test/build
+
+COPY script/build_boringssl.sh /build_boringssl.sh
+RUN git clone https://boringssl.googlesource.com/boringssl
+RUN ./build_boringssl.sh
+
+COPY script/build_nginx.sh /build_nginx.sh
+RUN git clone https://github.com/nginx/nginx
+RUN ./build_nginx.sh
+
+COPY vpp-data/lib/* /usr/lib/
+COPY resources/nginx/vcl.conf /vcl.conf
+COPY resources/nginx/nginx_http3.conf /nginx.conf
+COPY script/nginx_ldp.sh /usr/bin/nginx_ldp.sh
+
+COPY resources/nginx/html/index.html /usr/share/nginx/index.html
+
+ENV VCL_CONFIG=/vcl.conf
+ENV LDP=/usr/lib/libvcl_ldpreload.so
+ENV LDP_DEBUG=0
+ENV VCL_DEBUG=0
+ENV LDP_SID_BIT=8
+
+ENTRYPOINT ["nginx_ldp.sh", "/usr/local/nginx/sbin/nginx", "-c", "/nginx.conf"]
diff --git a/extras/hs-test/docker/Dockerfile.nginx-server b/extras/hs-test/docker/Dockerfile.nginx-server
new file mode 100644
index 00000000000..1971158131b
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.nginx-server
@@ -0,0 +1,12 @@
+ARG UBUNTU_VERSION
+
+FROM ubuntu:${UBUNTU_VERSION}
+
+RUN apt-get update \
+ && apt-get install -y nginx \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY resources/nginx/nginx_server_mirroring.conf /nginx.conf
+
+
+ENTRYPOINT ["nginx", "-c", "/nginx.conf"]
diff --git a/extras/hs-test/docker/Dockerfile.vpp b/extras/hs-test/docker/Dockerfile.vpp
new file mode 100644
index 00000000000..9d900e86772
--- /dev/null
+++ b/extras/hs-test/docker/Dockerfile.vpp
@@ -0,0 +1,33 @@
+ARG UBUNTU_VERSION
+
+FROM ubuntu:${UBUNTU_VERSION}
+
+RUN apt-get update \
+ && apt-get install -y openssl libapr1 libnuma1 libsubunit0 \
+ iproute2 libnl-3-dev libnl-route-3-dev python3 iputils-ping \
+ vim gdb libunwind-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV DIR=vpp-data/lib/vpp_plugins
+COPY \
+ $DIR/af_packet_plugin.so \
+ $DIR/hs_apps_plugin.so \
+ $DIR/http_plugin.so \
+ $DIR/unittest_plugin.so \
+ $DIR/quic_plugin.so \
+ $DIR/http_static_plugin.so \
+ $DIR/ping_plugin.so \
+ $DIR/nsim_plugin.so \
+ $DIR/prom_plugin.so \
+ $DIR/tlsopenssl_plugin.so \
+ /usr/lib/x86_64-linux-gnu/vpp_plugins/
+
+COPY vpp-data/bin/vpp /usr/bin/
+COPY vpp-data/bin/vppctl /usr/bin/
+COPY vpp-data/bin/vpp_echo /usr/bin/
+COPY vpp-data/bin/vcl_* /usr/bin/
+COPY vpp-data/lib/*.so /usr/lib/
+
+RUN addgroup vpp
+
+ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/extras/hs-test/echo_test.go b/extras/hs-test/echo_test.go
new file mode 100644
index 00000000000..ce852bea3e0
--- /dev/null
+++ b/extras/hs-test/echo_test.go
@@ -0,0 +1,51 @@
+package main
+
+func init() {
+ registerVethTests(EchoBuiltinTest)
+ registerSoloVethTests(TcpWithLossTest)
+}
+
+func EchoBuiltinTest(s *VethsSuite) {
+ serverVpp := s.getContainerByName("server-vpp").vppInstance
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+
+ serverVpp.vppctl("test echo server " +
+ " uri tcp://" + serverVeth.ip4AddressString() + "/1234")
+
+ clientVpp := s.getContainerByName("client-vpp").vppInstance
+
+ o := clientVpp.vppctl("test echo client nclients 100 bytes 1 verbose" +
+ " syn-timeout 100 test-timeout 100" +
+ " uri tcp://" + serverVeth.ip4AddressString() + "/1234")
+ s.log(o)
+ s.assertNotContains(o, "failed:")
+}
+
+// unstable with multiple workers
+func TcpWithLossTest(s *VethsSuite) {
+ s.SkipIfMultiWorker()
+ serverVpp := s.getContainerByName("server-vpp").vppInstance
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ serverVpp.vppctl("test echo server uri tcp://%s/20022",
+ serverVeth.ip4AddressString())
+
+ clientVpp := s.getContainerByName("client-vpp").vppInstance
+
+ // Ensure that VPP doesn't abort itself with NSIM enabled
+ // Warning: Removing this ping will make VPP crash!
+ clientVpp.vppctl("ping %s", serverVeth.ip4AddressString())
+
+ // Add loss of packets with Network Delay Simulator
+ clientVpp.vppctl("set nsim poll-main-thread delay 0.01 ms bandwidth 40 gbit" +
+ " packet-size 1400 packets-per-drop 1000")
+
+ clientVpp.vppctl("nsim output-feature enable-disable host-" + s.getInterfaceByName(clientInterfaceName).name)
+
+ // Do echo test from client-vpp container
+ output := clientVpp.vppctl("test echo client uri tcp://%s/20022 verbose echo-bytes mbytes 50",
+ serverVeth.ip4AddressString())
+ s.log(output)
+ s.assertNotEqual(len(output), 0)
+ s.assertNotContains(output, "failed", output)
+}
diff --git a/extras/hs-test/framework_test.go b/extras/hs-test/framework_test.go
new file mode 100644
index 00000000000..8cbf936f026
--- /dev/null
+++ b/extras/hs-test/framework_test.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var suiteTimeout time.Duration
+
+func TestHst(t *testing.T) {
+ if *isVppDebug {
+ // 30 minute timeout so that the framework won't timeout while debugging
+ suiteTimeout = time.Minute * 30
+ } else {
+ suiteTimeout = time.Minute * 5
+ }
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "HST")
+}
diff --git a/extras/hs-test/go.mod b/extras/hs-test/go.mod
new file mode 100644
index 00000000000..3be9ba20a86
--- /dev/null
+++ b/extras/hs-test/go.mod
@@ -0,0 +1,33 @@
+module fd.io/hs-test
+
+go 1.21
+
+require (
+ github.com/edwarnicke/exechelper v1.0.3
+ go.fd.io/govpp v0.10.0
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
+ golang.org/x/net v0.20.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.17.0 // indirect
+)
+
+require (
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
+ github.com/onsi/ginkgo/v2 v2.16.0
+ github.com/onsi/gomega v1.32.0
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sirupsen/logrus v1.9.3
+ github.com/vishvananda/netns v0.0.4 // indirect
+ golang.org/x/sys v0.16.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+)
diff --git a/extras/hs-test/go.sum b/extras/hs-test/go.sum
new file mode 100644
index 00000000000..479b0289814
--- /dev/null
+++ b/extras/hs-test/go.sum
@@ -0,0 +1,70 @@
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/edwarnicke/exechelper v1.0.3 h1:OY2ocGAITTqnEDvZk0dRQSeMIQvyH0SyL/4ncz+5GeQ=
+github.com/edwarnicke/exechelper v1.0.3/go.mod h1:R65OUPKns4bgeHkCmfSHbmqLBU8aHZxTgLmEyUBUk4U=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
+github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
+github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
+github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
+github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
+github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+go.fd.io/govpp v0.10.0 h1:lL93SbqOILjON2pMvazrlHRekGYTRy0Qmj57RuAkxR0=
+go.fd.io/govpp v0.10.0/go.mod h1:5m3bZM9ck+2EGC2O3ASmSSJAaoouyOlVWtiwj5BdCv0=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/extras/hs-test/hst_suite.go b/extras/hs-test/hst_suite.go
new file mode 100644
index 00000000000..26dcf4e5a2b
--- /dev/null
+++ b/extras/hs-test/hst_suite.go
@@ -0,0 +1,502 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/edwarnicke/exechelper"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ DEFAULT_NETWORK_NUM int = 1
+)
+
+var isPersistent = flag.Bool("persist", false, "persists topology config")
+var isVerbose = flag.Bool("verbose", false, "verbose test output")
+var isUnconfiguring = flag.Bool("unconfigure", false, "remove topology")
+var isVppDebug = flag.Bool("debug", false, "attach gdb to vpp")
+var nConfiguredCpus = flag.Int("cpus", 1, "number of CPUs assigned to vpp")
+var vppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
+
+type HstSuite struct {
+ containers map[string]*Container
+ startedContainers []*Container
+ volumes []string
+ netConfigs []NetConfig
+ netInterfaces map[string]*NetInterface
+ ip4AddrAllocator *Ip4AddressAllocator
+ testIds map[string]string
+ cpuAllocator *CpuAllocatorT
+ cpuContexts []*CpuContext
+ cpuPerVpp int
+ pid string
+ logger *log.Logger
+ logFile *os.File
+}
+
+func (s *HstSuite) SetupSuite() {
+ s.createLogger()
+ s.log("Suite Setup")
+ RegisterFailHandler(func(message string, callerSkip ...int) {
+ s.hstFail()
+ Fail(message, callerSkip...)
+ })
+ var err error
+ s.pid = fmt.Sprint(os.Getpid())
+ s.cpuAllocator, err = CpuAllocator()
+ if err != nil {
+ Fail("failed to init cpu allocator: " + fmt.Sprint(err))
+ }
+ s.cpuPerVpp = *nConfiguredCpus
+}
+
+func (s *HstSuite) AllocateCpus() []int {
+ cpuCtx, err := s.cpuAllocator.Allocate(len(s.startedContainers), s.cpuPerVpp)
+ s.assertNil(err)
+ s.AddCpuContext(cpuCtx)
+ return cpuCtx.cpus
+}
+
+func (s *HstSuite) AddCpuContext(cpuCtx *CpuContext) {
+ s.cpuContexts = append(s.cpuContexts, cpuCtx)
+}
+
+func (s *HstSuite) TearDownSuite() {
+ defer s.logFile.Close()
+ s.log("Suite Teardown")
+ s.unconfigureNetworkTopology()
+}
+
+func (s *HstSuite) TearDownTest() {
+ s.log("Test Teardown")
+ if *isPersistent {
+ return
+ }
+ s.resetContainers()
+ s.removeVolumes()
+ s.ip4AddrAllocator.deleteIpAddresses()
+}
+
+func (s *HstSuite) skipIfUnconfiguring() {
+ if *isUnconfiguring {
+ s.skip("skipping to unconfigure")
+ }
+}
+
+func (s *HstSuite) SetupTest() {
+ s.log("Test Setup")
+ s.startedContainers = s.startedContainers[:0]
+ s.skipIfUnconfiguring()
+ s.setupVolumes()
+ s.setupContainers()
+}
+
+func (s *HstSuite) setupVolumes() {
+ for _, volume := range s.volumes {
+ cmd := "docker volume create --name=" + volume
+ s.log(cmd)
+ exechelper.Run(cmd)
+ }
+}
+
+func (s *HstSuite) setupContainers() {
+ for _, container := range s.containers {
+ if !container.isOptional {
+ container.run()
+ }
+ }
+}
+
+func (s *HstSuite) logVppInstance(container *Container, maxLines int) {
+ if container.vppInstance == nil {
+ return
+ }
+
+ logSource := container.getHostWorkDir() + defaultLogFilePath
+ file, err := os.Open(logSource)
+
+ if err != nil {
+ return
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ var lines []string
+ var counter int
+
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ counter++
+ if counter > maxLines {
+ lines = lines[1:]
+ counter--
+ }
+ }
+
+ s.log("vvvvvvvvvvvvvvv " + container.name + " [VPP instance]:")
+ for _, line := range lines {
+ s.log(line)
+ }
+ s.log("^^^^^^^^^^^^^^^\n\n")
+}
+
+func (s *HstSuite) hstFail() {
+ for _, container := range s.startedContainers {
+ out, err := container.log(20)
+ if err != nil {
+ s.log("An error occured while obtaining '" + container.name + "' container logs: " + fmt.Sprint(err))
+ s.log("The container might not be running - check logs in " + container.getLogDirPath())
+ continue
+ }
+ s.log("\nvvvvvvvvvvvvvvv " +
+ container.name + ":\n" +
+ out +
+ "^^^^^^^^^^^^^^^\n\n")
+ s.logVppInstance(container, 20)
+ }
+}
+
+func (s *HstSuite) assertNil(object interface{}, msgAndArgs ...interface{}) {
+ Expect(object).To(BeNil(), msgAndArgs...)
+}
+
+func (s *HstSuite) assertNotNil(object interface{}, msgAndArgs ...interface{}) {
+ Expect(object).ToNot(BeNil(), msgAndArgs...)
+}
+
+func (s *HstSuite) assertEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
+ Expect(actual).To(Equal(expected), msgAndArgs...)
+}
+
+func (s *HstSuite) assertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
+ Expect(actual).ToNot(Equal(expected), msgAndArgs...)
+}
+
+func (s *HstSuite) assertContains(testString, contains interface{}, msgAndArgs ...interface{}) {
+ Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
+}
+
+func (s *HstSuite) assertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) {
+ Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
+}
+
+func (s *HstSuite) assertNotEmpty(object interface{}, msgAndArgs ...interface{}) {
+ Expect(object).ToNot(BeEmpty(), msgAndArgs...)
+}
+
+func (s *HstSuite) createLogger() {
+ suiteName := CurrentSpecReport().ContainerHierarchyTexts[0]
+ var err error
+ s.logFile, err = os.Create("summary/" + suiteName + ".log")
+ if err != nil {
+ Fail("Unable to create log file.")
+ }
+ s.logger = log.New(io.Writer(s.logFile), "", log.LstdFlags)
+}
+
+// Logs to files by default, logs to stdout when VERBOSE=true with GinkgoWriter
+// to keep console tidy
+func (s *HstSuite) log(arg any) {
+ logs := strings.Split(fmt.Sprint(arg), "\n")
+ for _, line := range logs {
+ s.logger.Println(line)
+ }
+ if *isVerbose {
+ GinkgoWriter.Println(arg)
+ }
+}
+
+func (s *HstSuite) skip(args string) {
+ Skip(args)
+}
+
+func (s *HstSuite) SkipIfMultiWorker(args ...any) {
+ if *nConfiguredCpus > 1 {
+ s.skip("test case not supported with multiple vpp workers")
+ }
+}
+
+func (s *HstSuite) SkipUnlessExtendedTestsBuilt() {
+ imageName := "hs-test/nginx-http3"
+
+ cmd := exec.Command("docker", "images", imageName)
+ byteOutput, err := cmd.CombinedOutput()
+ if err != nil {
+ s.log("error while searching for docker image")
+ return
+ }
+ if !strings.Contains(string(byteOutput), imageName) {
+ s.skip("extended tests not built")
+ }
+}
+
+func (s *HstSuite) resetContainers() {
+ for _, container := range s.containers {
+ container.stop()
+ }
+}
+
+func (s *HstSuite) removeVolumes() {
+ for _, volumeName := range s.volumes {
+ cmd := "docker volume rm " + volumeName
+ exechelper.Run(cmd)
+ os.RemoveAll(volumeName)
+ }
+}
+
+func (s *HstSuite) getNetNamespaceByName(name string) string {
+ return name + s.pid
+}
+
+func (s *HstSuite) getInterfaceByName(name string) *NetInterface {
+ return s.netInterfaces[name+s.pid]
+}
+
+func (s *HstSuite) getContainerByName(name string) *Container {
+ return s.containers[name+s.pid]
+}
+
+/*
+ * Create a copy and return its address, so that individial tests which call this
+ * are not able to modify the original container and affect other tests by doing that
+ */
+func (s *HstSuite) getTransientContainerByName(name string) *Container {
+ containerCopy := *s.containers[name+s.pid]
+ return &containerCopy
+}
+
+func (s *HstSuite) loadContainerTopology(topologyName string) {
+ data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml")
+ if err != nil {
+ Fail("read error: " + fmt.Sprint(err))
+ }
+ var yamlTopo YamlTopology
+ err = yaml.Unmarshal(data, &yamlTopo)
+ if err != nil {
+ Fail("unmarshal error: " + fmt.Sprint(err))
+ }
+
+ for _, elem := range yamlTopo.Volumes {
+ volumeMap := elem["volume"].(VolumeConfig)
+ hostDir := volumeMap["host-dir"].(string)
+ workingVolumeDir := logDir + CurrentSpecReport().LeafNodeText + volumeDir
+ volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
+ hostDir = volDirReplacer.Replace(hostDir)
+ s.volumes = append(s.volumes, hostDir)
+ }
+
+ s.containers = make(map[string]*Container)
+ for _, elem := range yamlTopo.Containers {
+ newContainer, err := newContainer(s, elem)
+ newContainer.suite = s
+ newContainer.name += newContainer.suite.pid
+ if err != nil {
+ Fail("container config error: " + fmt.Sprint(err))
+ }
+ s.containers[newContainer.name] = newContainer
+ }
+}
+
+func (s *HstSuite) loadNetworkTopology(topologyName string) {
+ data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml")
+ if err != nil {
+ Fail("read error: " + fmt.Sprint(err))
+ }
+ var yamlTopo YamlTopology
+ err = yaml.Unmarshal(data, &yamlTopo)
+ if err != nil {
+ Fail("unmarshal error: " + fmt.Sprint(err))
+ }
+
+ s.ip4AddrAllocator = NewIp4AddressAllocator()
+ s.netInterfaces = make(map[string]*NetInterface)
+
+ for _, elem := range yamlTopo.Devices {
+ if _, ok := elem["name"]; ok {
+ elem["name"] = elem["name"].(string) + s.pid
+ }
+
+ if peer, ok := elem["peer"].(NetDevConfig); ok {
+ if peer["name"].(string) != "" {
+ peer["name"] = peer["name"].(string) + s.pid
+ }
+ if _, ok := peer["netns"]; ok {
+ peer["netns"] = peer["netns"].(string) + s.pid
+ }
+ }
+
+ if _, ok := elem["netns"]; ok {
+ elem["netns"] = elem["netns"].(string) + s.pid
+ }
+
+ if _, ok := elem["interfaces"]; ok {
+ interfaceCount := len(elem["interfaces"].([]interface{}))
+ for i := 0; i < interfaceCount; i++ {
+ elem["interfaces"].([]interface{})[i] = elem["interfaces"].([]interface{})[i].(string) + s.pid
+ }
+ }
+
+ switch elem["type"].(string) {
+ case NetNs:
+ {
+ if namespace, err := newNetNamespace(elem); err == nil {
+ s.netConfigs = append(s.netConfigs, &namespace)
+ } else {
+ Fail("network config error: " + fmt.Sprint(err))
+ }
+ }
+ case Veth, Tap:
+ {
+ if netIf, err := newNetworkInterface(elem, s.ip4AddrAllocator); err == nil {
+ s.netConfigs = append(s.netConfigs, netIf)
+ s.netInterfaces[netIf.Name()] = netIf
+ } else {
+ Fail("network config error: " + fmt.Sprint(err))
+ }
+ }
+ case Bridge:
+ {
+ if bridge, err := newBridge(elem); err == nil {
+ s.netConfigs = append(s.netConfigs, &bridge)
+ } else {
+ Fail("network config error: " + fmt.Sprint(err))
+ }
+ }
+ }
+ }
+}
+
+func (s *HstSuite) configureNetworkTopology(topologyName string) {
+ s.loadNetworkTopology(topologyName)
+
+ if *isUnconfiguring {
+ return
+ }
+
+ for _, nc := range s.netConfigs {
+ if err := nc.configure(); err != nil {
+ Fail("Network config error: " + fmt.Sprint(err))
+ }
+ }
+}
+
+func (s *HstSuite) unconfigureNetworkTopology() {
+ if *isPersistent {
+ return
+ }
+ for _, nc := range s.netConfigs {
+ nc.unconfigure()
+ }
+}
+
+func (s *HstSuite) getTestId() string {
+ testName := CurrentSpecReport().LeafNodeText
+
+ if s.testIds == nil {
+ s.testIds = map[string]string{}
+ }
+
+ if _, ok := s.testIds[testName]; !ok {
+ s.testIds[testName] = time.Now().Format("2006-01-02_15-04-05")
+ }
+
+ return s.testIds[testName]
+}
+
+// Returns last 4 digits of PID
+func (s *HstSuite) getPortFromPid() string {
+ port := s.pid
+ for len(port) < 4 {
+ port += "0"
+ }
+ return port[len(port)-4:]
+}
+
+func (s *HstSuite) startServerApp(running chan error, done chan struct{}, env []string) {
+ cmd := exec.Command("iperf3", "-4", "-s", "-p", s.getPortFromPid())
+ if env != nil {
+ cmd.Env = env
+ }
+ s.log(cmd)
+ err := cmd.Start()
+ if err != nil {
+ msg := fmt.Errorf("failed to start iperf server: %v", err)
+ running <- msg
+ return
+ }
+ running <- nil
+ <-done
+ cmd.Process.Kill()
+}
+
+func (s *HstSuite) startClientApp(ipAddress string, env []string, clnCh chan error, clnRes chan string) {
+ defer func() {
+ clnCh <- nil
+ }()
+
+ nTries := 0
+
+ for {
+ cmd := exec.Command("iperf3", "-c", ipAddress, "-u", "-l", "1460", "-b", "10g", "-p", s.getPortFromPid())
+ if env != nil {
+ cmd.Env = env
+ }
+ s.log(cmd)
+ o, err := cmd.CombinedOutput()
+ if err != nil {
+ if nTries > 5 {
+ clnCh <- fmt.Errorf("failed to start client app '%s'.\n%s", err, o)
+ return
+ }
+ time.Sleep(1 * time.Second)
+ nTries++
+ continue
+ } else {
+ clnRes <- fmt.Sprintf("Client output: %s", o)
+ }
+ break
+ }
+}
+
+func (s *HstSuite) startHttpServer(running chan struct{}, done chan struct{}, addressPort, netNs string) {
+ cmd := newCommand([]string{"./http_server", addressPort, s.pid}, netNs)
+ err := cmd.Start()
+ s.log(cmd)
+ if err != nil {
+ s.log("Failed to start http server: " + fmt.Sprint(err))
+ return
+ }
+ running <- struct{}{}
+ <-done
+ cmd.Process.Kill()
+}
+
+func (s *HstSuite) startWget(finished chan error, server_ip, port, query, netNs string) {
+ defer func() {
+ finished <- errors.New("wget error")
+ }()
+
+ cmd := newCommand([]string{"wget", "--timeout=10", "--no-proxy", "--tries=5", "-O", "/dev/null", server_ip + ":" + port + "/" + query},
+ netNs)
+ s.log(cmd)
+ o, err := cmd.CombinedOutput()
+ if err != nil {
+ finished <- fmt.Errorf("wget error: '%v\n\n%s'", err, o)
+ return
+ } else if !strings.Contains(string(o), "200 OK") {
+ finished <- fmt.Errorf("wget error: response not 200 OK")
+ return
+ }
+ finished <- nil
+}
diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go
new file mode 100644
index 00000000000..0bb6a43e851
--- /dev/null
+++ b/extras/hs-test/http_test.go
@@ -0,0 +1,295 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+func init() {
+ registerNsTests(HttpTpsTest)
+ registerVethTests(HttpCliTest, HttpCliConnectErrorTest)
+ registerNoTopoTests(NginxHttp3Test, NginxAsServerTest,
+ NginxPerfCpsTest, NginxPerfRpsTest, NginxPerfWrkTest, HeaderServerTest,
+ HttpStaticMovedTest, HttpStaticNotFoundTest, HttpCliMethodNotAllowedTest,
+ HttpCliBadRequestTest, HttpStaticPathTraversalTest)
+ registerNoTopoSoloTests(HttpStaticPromTest)
+}
+
+const wwwRootPath = "/tmp/www_root"
+
+func HttpTpsTest(s *NsSuite) {
+ iface := s.getInterfaceByName(clientInterface)
+ client_ip := iface.ip4AddressString()
+ port := "8080"
+ finished := make(chan error, 1)
+ clientNetns := s.getNetNamespaceByName("cln")
+
+ container := s.getContainerByName("vpp")
+
+ // configure vpp in the container
+ container.vppInstance.vppctl("http tps uri tcp://0.0.0.0/8080")
+
+ go func() {
+ defer GinkgoRecover()
+ s.startWget(finished, client_ip, port, "test_file_10M", clientNetns)
+ }()
+ // wait for client
+ err := <-finished
+ s.assertNil(err, fmt.Sprint(err))
+}
+
+func HttpCliTest(s *VethsSuite) {
+ serverContainer := s.getContainerByName("server-vpp")
+ clientContainer := s.getContainerByName("client-vpp")
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+
+ serverContainer.vppInstance.vppctl("http cli server")
+
+ uri := "http://" + serverVeth.ip4AddressString() + "/80"
+
+ o := clientContainer.vppInstance.vppctl("http cli client" +
+ " uri " + uri + " query /show/vlib/graph")
+
+ s.log(o)
+ s.assertContains(o, "<html>", "<html> not found in the result!")
+}
+
+func HttpCliConnectErrorTest(s *VethsSuite) {
+ clientContainer := s.getContainerByName("client-vpp")
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+
+ uri := "http://" + serverVeth.ip4AddressString() + "/80"
+
+ o := clientContainer.vppInstance.vppctl("http cli client" +
+ " uri " + uri + " query /show/vlib/graph")
+
+ s.log(o)
+ s.assertContains(o, "failed to connect")
+}
+
+func NginxHttp3Test(s *NoTopoSuite) {
+ s.SkipUnlessExtendedTestsBuilt()
+
+ query := "index.html"
+ nginxCont := s.getContainerByName("nginx-http3")
+ s.assertNil(nginxCont.run())
+
+ vpp := s.getContainerByName("vpp").vppInstance
+ vpp.waitForApp("nginx-", 5)
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+
+ defer func() { os.Remove(query) }()
+ curlCont := s.getContainerByName("curl")
+ args := fmt.Sprintf("curl --noproxy '*' --local-port 55444 --http3-only -k https://%s:8443/%s", serverAddress, query)
+ curlCont.extraRunningArgs = args
+ o, err := curlCont.combinedOutput()
+ s.assertNil(err, fmt.Sprint(err))
+ s.assertContains(o, "<http>", "<http> not found in the result!")
+}
+
+func HttpStaticPromTest(s *NoTopoSuite) {
+ finished := make(chan error, 1)
+ query := "stats.prom"
+ vpp := s.getContainerByName("vpp").vppInstance
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers"))
+ s.log(vpp.vppctl("prom enable"))
+ time.Sleep(time.Second * 5)
+ go func() {
+ defer GinkgoRecover()
+ s.startWget(finished, serverAddress, "80", query, "")
+ }()
+ err := <-finished
+ s.assertNil(err)
+}
+
+func HttpStaticPathTraversalTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ vpp.container.exec("mkdir -p " + wwwRootPath)
+ vpp.container.exec("mkdir -p " + "/tmp/secret_folder")
+ vpp.container.createFile("/tmp/secret_folder/secret_file.txt", "secret")
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug"))
+
+ client := newHttpClient()
+ req, err := http.NewRequest("GET", "http://"+serverAddress+":80/../secret_folder/secret_file.txt", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual(404, resp.StatusCode)
+}
+
+func HttpStaticMovedTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ vpp.container.exec("mkdir -p " + wwwRootPath + "/tmp.aaa")
+ vpp.container.createFile(wwwRootPath+"/tmp.aaa/index.html", "<http><body><p>Hello</p></body></http>")
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug"))
+
+ client := newHttpClient()
+ req, err := http.NewRequest("GET", "http://"+serverAddress+":80/tmp.aaa", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual(301, resp.StatusCode)
+ s.assertNotEqual("", resp.Header.Get("Location"))
+}
+
+func HttpStaticNotFoundTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ vpp.container.exec("mkdir -p " + wwwRootPath)
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ s.log(vpp.vppctl("http static server www-root " + wwwRootPath + " uri tcp://" + serverAddress + "/80 debug"))
+
+ client := newHttpClient()
+ req, err := http.NewRequest("GET", "http://"+serverAddress+":80/notfound.html", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual(404, resp.StatusCode)
+}
+
+func HttpCliMethodNotAllowedTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ vpp.vppctl("http cli server")
+
+ client := newHttpClient()
+ req, err := http.NewRequest("POST", "http://"+serverAddress+":80/test", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual(405, resp.StatusCode)
+ // TODO: need to be fixed in http code
+ //s.assertNotEqual("", resp.Header.Get("Allow"))
+}
+
+func HttpCliBadRequestTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ vpp.vppctl("http cli server")
+
+ client := newHttpClient()
+ req, err := http.NewRequest("GET", "http://"+serverAddress+":80", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual(400, resp.StatusCode)
+}
+
+func HeaderServerTest(s *NoTopoSuite) {
+ vpp := s.getContainerByName("vpp").vppInstance
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+ vpp.vppctl("http cli server")
+
+ client := newHttpClient()
+ req, err := http.NewRequest("GET", "http://"+serverAddress+":80/show/version", nil)
+ s.assertNil(err, fmt.Sprint(err))
+ resp, err := client.Do(req)
+ s.assertNil(err, fmt.Sprint(err))
+ defer resp.Body.Close()
+ s.assertEqual("http_cli_server", resp.Header.Get("Server"))
+}
+
+func NginxAsServerTest(s *NoTopoSuite) {
+ s.skip("Broken in the CI")
+ query := "return_ok"
+ finished := make(chan error, 1)
+
+ nginxCont := s.getContainerByName("nginx")
+ s.assertNil(nginxCont.run())
+
+ vpp := s.getContainerByName("vpp").vppInstance
+ vpp.waitForApp("nginx-", 5)
+
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+
+ defer func() { os.Remove(query) }()
+ go func() {
+ defer GinkgoRecover()
+ s.startWget(finished, serverAddress, "80", query, "")
+ }()
+ s.assertNil(<-finished)
+}
+
+func parseString(s, pattern string) string {
+ temp := strings.Split(s, "\n")
+ for _, item := range temp {
+ if strings.Contains(item, pattern) {
+ return item
+ }
+ }
+ return ""
+}
+
+func runNginxPerf(s *NoTopoSuite, mode, ab_or_wrk string) error {
+ nRequests := 1000000
+ nClients := 1000
+
+ serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
+
+ vpp := s.getContainerByName("vpp").vppInstance
+
+ nginxCont := s.getContainerByName(singleTopoContainerNginx)
+ s.assertNil(nginxCont.run())
+ vpp.waitForApp("nginx-", 5)
+
+ if ab_or_wrk == "ab" {
+ abCont := s.getContainerByName("ab")
+ args := fmt.Sprintf("-n %d -c %d", nRequests, nClients)
+ if mode == "rps" {
+ args += " -k"
+ } else if mode != "cps" {
+ return fmt.Errorf("invalid mode %s; expected cps/rps", mode)
+ }
+ // don't exit on socket receive errors
+ args += " -r"
+ args += " http://" + serverAddress + ":80/64B.json"
+ abCont.extraRunningArgs = args
+ o, err := abCont.combinedOutput()
+ rps := parseString(o, "Requests per second:")
+ s.log(rps)
+ s.log(err)
+ s.assertNil(err, "err: '%s', output: '%s'", err, o)
+ } else {
+ wrkCont := s.getContainerByName("wrk")
+ args := fmt.Sprintf("-c %d -t 2 -d 30 http://%s:80/64B.json", nClients,
+ serverAddress)
+ wrkCont.extraRunningArgs = args
+ o, err := wrkCont.combinedOutput()
+ rps := parseString(o, "requests")
+ s.log(rps)
+ s.log(err)
+ s.assertNil(err, "err: '%s', output: '%s'", err, o)
+ }
+ return nil
+}
+
+// unstable with multiple workers
+func NginxPerfCpsTest(s *NoTopoSuite) {
+ s.skip("Broken in the CI")
+ s.SkipIfMultiWorker()
+ s.assertNil(runNginxPerf(s, "cps", "ab"))
+}
+
+func NginxPerfRpsTest(s *NoTopoSuite) {
+ s.skip("Broken in the CI")
+ s.assertNil(runNginxPerf(s, "rps", "ab"))
+}
+
+func NginxPerfWrkTest(s *NoTopoSuite) {
+ s.skip("Broken in the CI")
+ s.assertNil(runNginxPerf(s, "", "wrk"))
+}
diff --git a/extras/hs-test/ldp_test.go b/extras/hs-test/ldp_test.go
new file mode 100644
index 00000000000..24d2de39485
--- /dev/null
+++ b/extras/hs-test/ldp_test.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+func init() {
+ registerVethTests(LDPreloadIperfVppTest)
+}
+
+func LDPreloadIperfVppTest(s *VethsSuite) {
+ var clnVclConf, srvVclConf Stanza
+
+ serverContainer := s.getContainerByName("server-vpp")
+ serverVclFileName := serverContainer.getHostWorkDir() + "/vcl_srv.conf"
+
+ clientContainer := s.getContainerByName("client-vpp")
+ clientVclFileName := clientContainer.getHostWorkDir() + "/vcl_cln.conf"
+
+ ldpreload := "LD_PRELOAD=../../build-root/build-vpp-native/vpp/lib/x86_64-linux-gnu/libvcl_ldpreload.so"
+
+ stopServerCh := make(chan struct{}, 1)
+ srvCh := make(chan error, 1)
+ clnCh := make(chan error)
+
+ s.log("starting VPPs")
+
+ clientAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default",
+ clientContainer.getHostWorkDir())
+ err := clnVclConf.
+ newStanza("vcl").
+ append("rx-fifo-size 4000000").
+ append("tx-fifo-size 4000000").
+ append("app-scope-local").
+ append("app-scope-global").
+ append("use-mq-eventfd").
+ append(clientAppSocketApi).close().
+ saveToFile(clientVclFileName)
+ s.assertNil(err, fmt.Sprint(err))
+
+ serverAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default",
+ serverContainer.getHostWorkDir())
+ err = srvVclConf.
+ newStanza("vcl").
+ append("rx-fifo-size 4000000").
+ append("tx-fifo-size 4000000").
+ append("app-scope-local").
+ append("app-scope-global").
+ append("use-mq-eventfd").
+ append(serverAppSocketApi).close().
+ saveToFile(serverVclFileName)
+ s.assertNil(err, fmt.Sprint(err))
+
+ s.log("attaching server to vpp")
+
+ srvEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+serverVclFileName)
+ go func() {
+ defer GinkgoRecover()
+ s.startServerApp(srvCh, stopServerCh, srvEnv)
+ }()
+
+ err = <-srvCh
+ s.assertNil(err, fmt.Sprint(err))
+
+ s.log("attaching client to vpp")
+ var clnRes = make(chan string, 1)
+ clnEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+clientVclFileName)
+ serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString()
+ go func() {
+ defer GinkgoRecover()
+ s.startClientApp(serverVethAddress, clnEnv, clnCh, clnRes)
+ }()
+ s.log(<-clnRes)
+
+ // wait for client's result
+ err = <-clnCh
+ s.assertNil(err, fmt.Sprint(err))
+
+ // stop server
+ stopServerCh <- struct{}{}
+}
diff --git a/extras/hs-test/linux_iperf_test.go b/extras/hs-test/linux_iperf_test.go
new file mode 100644
index 00000000000..e323f7fb721
--- /dev/null
+++ b/extras/hs-test/linux_iperf_test.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+func init() {
+ registerTapTests(LinuxIperfTest)
+}
+
+func LinuxIperfTest(s *TapSuite) {
+ clnCh := make(chan error)
+ stopServerCh := make(chan struct{})
+ srvCh := make(chan error, 1)
+ clnRes := make(chan string, 1)
+ defer func() {
+ stopServerCh <- struct{}{}
+ }()
+
+ go func() {
+ defer GinkgoRecover()
+ s.startServerApp(srvCh, stopServerCh, nil)
+ }()
+ err := <-srvCh
+ s.assertNil(err, fmt.Sprint(err))
+ s.log("server running")
+
+ ipAddress := s.getInterfaceByName(tapInterfaceName).ip4AddressString()
+ go func() {
+ defer GinkgoRecover()
+ s.startClientApp(ipAddress, nil, clnCh, clnRes)
+ }()
+ s.log("client running")
+ s.log(<-clnRes)
+ err = <-clnCh
+ s.assertNil(err, "err: '%s', ip: '%s'", err, ipAddress)
+ s.log("Test completed")
+}
diff --git a/extras/hs-test/mirroring_test.go b/extras/hs-test/mirroring_test.go
new file mode 100644
index 00000000000..daf3fc913e0
--- /dev/null
+++ b/extras/hs-test/mirroring_test.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "github.com/edwarnicke/exechelper"
+)
+
+func init() {
+ // registerNginxTests(MirroringTest)
+}
+
+// broken when CPUS > 1
+func MirroringTest(s *NginxSuite) {
+ s.skip("Broken in the CI")
+ s.SkipIfMultiWorker()
+ proxyAddress := s.getInterfaceByName(mirroringClientInterfaceName).peer.ip4AddressString()
+
+ path := "/64B.json"
+
+ testCommand := "wrk -c 20 -t 10 -d 10 http://" + proxyAddress + ":80" + path
+ s.log(testCommand)
+ o, _ := exechelper.Output(testCommand)
+ s.log(string(o))
+ s.assertNotEmpty(o)
+
+ vppProxyContainer := s.getContainerByName(vppProxyContainerName)
+ s.assertEqual(0, vppProxyContainer.vppInstance.GetSessionStat("no lcl port"))
+}
diff --git a/extras/hs-test/netconfig.go b/extras/hs-test/netconfig.go
new file mode 100644
index 00000000000..c76a0fda5f5
--- /dev/null
+++ b/extras/hs-test/netconfig.go
@@ -0,0 +1,383 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os/exec"
+ "strings"
+
+ "go.fd.io/govpp/binapi/ethernet_types"
+ "go.fd.io/govpp/binapi/interface_types"
+ "go.fd.io/govpp/binapi/ip_types"
+)
+
+type (
+ Cmd = exec.Cmd
+ MacAddress = ethernet_types.MacAddress
+ AddressWithPrefix = ip_types.AddressWithPrefix
+ IP4AddressWithPrefix = ip_types.IP4AddressWithPrefix
+ InterfaceIndex = interface_types.InterfaceIndex
+
+ NetConfig interface {
+ configure() error
+ unconfigure()
+ Name() string
+ Type() string
+ }
+
+ NetConfigBase struct {
+ name string
+ category string // what else to call this when `type` is reserved?
+ }
+
+ NetInterface struct {
+ NetConfigBase
+ ip4AddrAllocator *Ip4AddressAllocator
+ ip4Address string
+ index InterfaceIndex
+ hwAddress MacAddress
+ networkNamespace string
+ networkNumber int
+ peer *NetInterface
+ }
+
+ NetworkNamespace struct {
+ NetConfigBase
+ }
+
+ NetworkBridge struct {
+ NetConfigBase
+ networkNamespace string
+ interfaces []string
+ }
+)
+
+const (
+ NetNs string = "netns"
+ Veth string = "veth"
+ Tap string = "tap"
+ Bridge string = "bridge"
+)
+
+type InterfaceAdder func(n *NetInterface) *Cmd
+
+var (
+ ipCommandMap = map[string]InterfaceAdder{
+ Veth: func(n *NetInterface) *Cmd {
+ return exec.Command("ip", "link", "add", n.name, "type", "veth", "peer", "name", n.peer.name)
+ },
+ Tap: func(n *NetInterface) *Cmd {
+ return exec.Command("ip", "tuntap", "add", n.name, "mode", "tap")
+ },
+ }
+)
+
+func newNetworkInterface(cfg NetDevConfig, a *Ip4AddressAllocator) (*NetInterface, error) {
+ var newInterface *NetInterface = &NetInterface{}
+ var err error
+ newInterface.ip4AddrAllocator = a
+ newInterface.name = cfg["name"].(string)
+ newInterface.networkNumber = DEFAULT_NETWORK_NUM
+
+ if interfaceType, ok := cfg["type"]; ok {
+ newInterface.category = interfaceType.(string)
+ }
+
+ if presetHwAddress, ok := cfg["preset-hw-address"]; ok {
+ newInterface.hwAddress, err = ethernet_types.ParseMacAddress(presetHwAddress.(string))
+ if err != nil {
+ return &NetInterface{}, err
+ }
+ }
+
+ if netns, ok := cfg["netns"]; ok {
+ newInterface.networkNamespace = netns.(string)
+ }
+
+ if ip, ok := cfg["ip4"]; ok {
+ if n, ok := ip.(NetDevConfig)["network"]; ok {
+ newInterface.networkNumber = n.(int)
+ }
+ newInterface.ip4Address, err = newInterface.ip4AddrAllocator.NewIp4InterfaceAddress(
+ newInterface.networkNumber,
+ )
+ if err != nil {
+ return &NetInterface{}, err
+ }
+ }
+
+ if _, ok := cfg["peer"]; !ok {
+ return newInterface, nil
+ }
+
+ peer := cfg["peer"].(NetDevConfig)
+
+ if newInterface.peer, err = newNetworkInterface(peer, a); err != nil {
+ return &NetInterface{}, err
+ }
+
+ return newInterface, nil
+}
+
+func (n *NetInterface) configureUpState() error {
+ err := setDevUp(n.Name(), "")
+ if err != nil {
+ return fmt.Errorf("set link up failed: %v", err)
+ }
+ return nil
+}
+
+func (n *NetInterface) configureNetworkNamespace() error {
+ if n.networkNamespace != "" {
+ err := linkSetNetns(n.name, n.networkNamespace)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (n *NetInterface) configureAddress() error {
+ if n.ip4Address != "" {
+ if err := addAddress(
+ n.Name(),
+ n.ip4Address,
+ n.networkNamespace,
+ ); err != nil {
+ return err
+ }
+
+ }
+ return nil
+}
+
+func (n *NetInterface) configure() error {
+ cmd := ipCommandMap[n.Type()](n)
+ _, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("creating interface '%v' failed: %v", n.Name(), err)
+ }
+
+ if err := n.configureUpState(); err != nil {
+ return err
+ }
+
+ if err := n.configureNetworkNamespace(); err != nil {
+ return err
+ }
+
+ if err := n.configureAddress(); err != nil {
+ return err
+ }
+
+ if n.peer != nil && n.peer.name != "" {
+ if err := n.peer.configureUpState(); err != nil {
+ return err
+ }
+
+ if err := n.peer.configureNetworkNamespace(); err != nil {
+ return err
+ }
+
+ if err := n.peer.configureAddress(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (n *NetInterface) unconfigure() {
+ delLink(n.name)
+}
+
+func (n *NetInterface) Name() string {
+ return n.name
+}
+
+func (n *NetInterface) Type() string {
+ return n.category
+}
+
+func (n *NetInterface) addressWithPrefix() AddressWithPrefix {
+ address, _ := ip_types.ParseAddressWithPrefix(n.ip4Address)
+ return address
+}
+
+func (n *NetInterface) ip4AddressWithPrefix() IP4AddressWithPrefix {
+ ip4Prefix, _ := ip_types.ParseIP4Prefix(n.ip4Address)
+ ip4AddressWithPrefix := ip_types.IP4AddressWithPrefix(ip4Prefix)
+ return ip4AddressWithPrefix
+}
+
+func (n *NetInterface) ip4AddressString() string {
+ return strings.Split(n.ip4Address, "/")[0]
+}
+
+func (b *NetConfigBase) Name() string {
+ return b.name
+}
+
+func (b *NetConfigBase) Type() string {
+ return b.category
+}
+
+func newNetNamespace(cfg NetDevConfig) (NetworkNamespace, error) {
+ var networkNamespace NetworkNamespace
+ networkNamespace.name = cfg["name"].(string)
+ networkNamespace.category = NetNs
+ return networkNamespace, nil
+}
+
+func (ns *NetworkNamespace) configure() error {
+ return addDelNetns(ns.name, true)
+}
+
+func (ns *NetworkNamespace) unconfigure() {
+ addDelNetns(ns.name, false)
+}
+
+func newBridge(cfg NetDevConfig) (NetworkBridge, error) {
+ var bridge NetworkBridge
+ bridge.name = cfg["name"].(string)
+ bridge.category = Bridge
+ for _, v := range cfg["interfaces"].([]interface{}) {
+ bridge.interfaces = append(bridge.interfaces, v.(string))
+ }
+
+ bridge.networkNamespace = ""
+ if netns, ok := cfg["netns"]; ok {
+ bridge.networkNamespace = netns.(string)
+ }
+ return bridge, nil
+}
+
+func (b *NetworkBridge) configure() error {
+ return addBridge(b.name, b.interfaces, b.networkNamespace)
+}
+
+func (b *NetworkBridge) unconfigure() {
+ delBridge(b.name, b.networkNamespace)
+}
+
+func delBridge(brName, ns string) error {
+ err := setDevDown(brName, ns)
+ if err != nil {
+ return err
+ }
+
+ err = addDelBridge(brName, ns, false)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func setDevUp(dev, ns string) error {
+ return setDevUpDown(dev, ns, true)
+}
+
+func setDevDown(dev, ns string) error {
+ return setDevUpDown(dev, ns, false)
+}
+
+func delLink(ifName string) {
+ cmd := exec.Command("ip", "link", "del", ifName)
+ cmd.Run()
+}
+
+func setDevUpDown(dev, ns string, isUp bool) error {
+ var op string
+ if isUp {
+ op = "up"
+ } else {
+ op = "down"
+ }
+ c := []string{"ip", "link", "set", "dev", dev, op}
+ cmd := appendNetns(c, ns)
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("error bringing %s device %s! (cmd: '%s')", dev, op, cmd)
+ }
+ return nil
+}
+
+func addDelNetns(name string, isAdd bool) error {
+ var op string
+ if isAdd {
+ op = "add"
+ } else {
+ op = "del"
+ }
+ cmd := exec.Command("ip", "netns", op, name)
+ _, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("add/del netns failed (cmd: '%s')", cmd)
+ }
+ return nil
+}
+
+func linkSetNetns(ifName, ns string) error {
+ cmd := exec.Command("ip", "link", "set", "dev", ifName, "up", "netns", ns)
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("error setting device '%s' to netns '%s: %v", ifName, ns, err)
+ }
+ return nil
+}
+
+func newCommand(s []string, ns string) *exec.Cmd {
+ return appendNetns(s, ns)
+}
+
+func appendNetns(s []string, ns string) *exec.Cmd {
+ var cmd *exec.Cmd
+ if ns == "" {
+ // use default namespace
+ cmd = exec.Command(s[0], s[1:]...)
+ } else {
+ var args = []string{"netns", "exec", ns}
+ args = append(args, s[:]...)
+ cmd = exec.Command("ip", args...)
+ }
+ return cmd
+}
+
+func addDelBridge(brName, ns string, isAdd bool) error {
+ var op string
+ if isAdd {
+ op = "addbr"
+ } else {
+ op = "delbr"
+ }
+ var c = []string{"brctl", op, brName}
+ cmd := appendNetns(c, ns)
+ err := cmd.Run()
+ if err != nil {
+ s := fmt.Sprintf("%s %s failed! err: '%s'", op, brName, err)
+ return errors.New(s)
+ }
+ return nil
+}
+
+func addBridge(brName string, ifs []string, ns string) error {
+ err := addDelBridge(brName, ns, true)
+ if err != nil {
+ return err
+ }
+
+ for _, v := range ifs {
+ c := []string{"brctl", "addif", brName, v}
+ cmd := appendNetns(c, ns)
+ err = cmd.Run()
+ if err != nil {
+ return fmt.Errorf("error adding %s to bridge %s: %s", v, brName, err)
+ }
+ }
+ err = setDevUp(brName, ns)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/extras/hs-test/proxy_test.go b/extras/hs-test/proxy_test.go
new file mode 100644
index 00000000000..ac5f94c8535
--- /dev/null
+++ b/extras/hs-test/proxy_test.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/edwarnicke/exechelper"
+ . "github.com/onsi/ginkgo/v2"
+)
+
+func init() {
+ registerNsTests(VppProxyHttpTcpTest, VppProxyHttpTlsTest, EnvoyProxyHttpTcpTest)
+}
+
+func testProxyHttpTcp(s *NsSuite, proto string) error {
+ var outputFile string = "test" + s.pid + ".data"
+ var srcFilePid string = "httpTestFile" + s.pid
+ const srcFileNoPid = "httpTestFile"
+ const fileSize string = "10M"
+ stopServer := make(chan struct{}, 1)
+ serverRunning := make(chan struct{}, 1)
+ serverNetns := s.getNetNamespaceByName("srv")
+ clientNetns := s.getNetNamespaceByName("cln")
+
+ // create test file
+ err := exechelper.Run(fmt.Sprintf("ip netns exec %s truncate -s %s %s", serverNetns, fileSize, srcFilePid))
+ s.assertNil(err, "failed to run truncate command: "+fmt.Sprint(err))
+ defer func() { os.Remove(srcFilePid) }()
+
+ s.log("test file created...")
+
+ go func() {
+ defer GinkgoRecover()
+ s.startHttpServer(serverRunning, stopServer, ":666", serverNetns)
+ }()
+ // TODO better error handling and recovery
+ <-serverRunning
+
+ defer func(chan struct{}) {
+ stopServer <- struct{}{}
+ }(stopServer)
+
+ s.log("http server started...")
+
+ clientVeth := s.getInterfaceByName(clientInterface)
+ c := fmt.Sprintf("ip netns exec %s wget --no-proxy --retry-connrefused"+
+ " --retry-on-http-error=503 --tries=10 -O %s ", clientNetns, outputFile)
+ if proto == "tls" {
+ c += " --secure-protocol=TLSv1_3 --no-check-certificate https://"
+ }
+ c += fmt.Sprintf("%s:555/%s", clientVeth.ip4AddressString(), srcFileNoPid)
+ s.log(c)
+ _, err = exechelper.CombinedOutput(c)
+
+ defer func() { os.Remove(outputFile) }()
+
+ s.assertNil(err, "failed to run wget: '%s', cmd: %s", err, c)
+ stopServer <- struct{}{}
+
+ s.assertNil(assertFileSize(outputFile, srcFilePid))
+ return nil
+}
+
+func configureVppProxy(s *NsSuite, proto string) {
+ serverVeth := s.getInterfaceByName(serverInterface)
+ clientVeth := s.getInterfaceByName(clientInterface)
+
+ testVppProxy := s.getContainerByName("vpp").vppInstance
+ output := testVppProxy.vppctl(
+ "test proxy server server-uri %s://%s/555 client-uri tcp://%s/666",
+ proto,
+ clientVeth.ip4AddressString(),
+ serverVeth.peer.ip4AddressString(),
+ )
+ s.log("proxy configured: " + output)
+}
+
+func VppProxyHttpTcpTest(s *NsSuite) {
+ proto := "tcp"
+ configureVppProxy(s, proto)
+ err := testProxyHttpTcp(s, proto)
+ s.assertNil(err, fmt.Sprint(err))
+}
+
+func VppProxyHttpTlsTest(s *NsSuite) {
+ proto := "tls"
+ configureVppProxy(s, proto)
+ err := testProxyHttpTcp(s, proto)
+ s.assertNil(err, fmt.Sprint(err))
+}
+
+func configureEnvoyProxy(s *NsSuite) {
+ envoyContainer := s.getContainerByName("envoy")
+ err := envoyContainer.create()
+ s.assertNil(err, "Error creating envoy container: %s", err)
+
+ serverVeth := s.getInterfaceByName(serverInterface)
+ address := struct {
+ Server string
+ }{
+ Server: serverVeth.peer.ip4AddressString(),
+ }
+ envoyContainer.createConfig(
+ "/etc/envoy/envoy.yaml",
+ "resources/envoy/proxy.yaml",
+ address,
+ )
+ s.assertNil(envoyContainer.start())
+}
+
+func EnvoyProxyHttpTcpTest(s *NsSuite) {
+ configureEnvoyProxy(s)
+ err := testProxyHttpTcp(s, "tcp")
+ s.assertNil(err, fmt.Sprint(err))
+}
diff --git a/extras/hs-test/raw_session_test.go b/extras/hs-test/raw_session_test.go
new file mode 100644
index 00000000000..5c66df0b1ce
--- /dev/null
+++ b/extras/hs-test/raw_session_test.go
@@ -0,0 +1,40 @@
+package main
+
+func init() {
+ registerVethTests(VppEchoQuicTest, VppEchoTcpTest)
+}
+
+func VppEchoQuicTest(s *VethsSuite) {
+ s.testVppEcho("quic")
+}
+
+// TODO: udp echo currently broken in vpp
+func VppEchoUdpTest(s *VethsSuite) {
+ s.testVppEcho("udp")
+}
+
+func VppEchoTcpTest(s *VethsSuite) {
+ s.testVppEcho("tcp")
+}
+
+func (s *VethsSuite) testVppEcho(proto string) {
+ serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString()
+ uri := proto + "://" + serverVethAddress + "/12344"
+
+ echoSrvContainer := s.getContainerByName("server-app")
+ serverCommand := "vpp_echo server TX=RX" +
+ " socket-name " + echoSrvContainer.getContainerWorkDir() + "/var/run/app_ns_sockets/default" +
+ " use-app-socket-api" +
+ " uri " + uri
+ s.log(serverCommand)
+ echoSrvContainer.execServer(serverCommand)
+
+ echoClnContainer := s.getContainerByName("client-app")
+
+ clientCommand := "vpp_echo client" +
+ " socket-name " + echoClnContainer.getContainerWorkDir() + "/var/run/app_ns_sockets/default" +
+ " use-app-socket-api uri " + uri
+ s.log(clientCommand)
+ o := echoClnContainer.exec(clientCommand)
+ s.log(o)
+}
diff --git a/extras/hs-test/resources/cert/localhost.crt b/extras/hs-test/resources/cert/localhost.crt
new file mode 100644
index 00000000000..b21fb48906e
--- /dev/null
+++ b/extras/hs-test/resources/cert/localhost.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZTCCAk2gAwIBAgIUF116CAipHqQBCyAEvNesV0u4u0swDQYJKoZIhvcNAQEL
+BQAwQjELMAkGA1UEBhMCU0sxEDAOBgNVBAgMB1ZwcExhbmQxITAfBgNVBAoMGElu
+dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MjkxMDI0MjhaFw0yNDA1Mjgx
+MDI0MjhaMEIxCzAJBgNVBAYTAlNLMRAwDgYDVQQIDAdWcHBMYW5kMSEwHwYDVQQK
+DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCy40rDzrrHPGIyhP24hOBQefEgKD5uUGgSUyJTCur4yB/r2PGt
+LlfipKwDmNArmZuFOgKh8evipu2jYaxf4GHQmi7PGLddvPkqo5FWtVW8oAVJMcp+
+fwfs7OgkqtYD6Y7qjmjfXb9+rMpPN8WZ7cKbJwZpF3lf8GGaLqRmPiQg2j8qzcVy
+nz8cIwBZP8BJVclA9GIagijY7Zcmz0HnTPrPoLMeyLJOTqPMfkUYA2H2eHeISkQP
+BeoFoiwCI5eM35UiWiLyiv9Kojn4BHx6MLrfKBjV13WtcRMgYm5VftsWOZ92lmHm
+bpj9mGgtd84JWtWxs33oG4mNRSAeujf9AE5VAgMBAAGjUzBRMB0GA1UdDgQWBBTj
+s+A5M/Cao+0Phgg6xFBKIPxLqjAfBgNVHSMEGDAWgBTjs+A5M/Cao+0Phgg6xFBK
+IPxLqjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB3EcGDby5u
+cEGjgAFR18kH4ztnYUdZUrPI72sOFfjRLtJpx00n759SBawqNW1Y2a1QRd+GgUBK
+YpYd2gzWYFjf/4c5BN4SrjeZGnQ8N0YomqqGKvOQO0YdYK4i/lWJjLRaLiVBn9EX
+Z+odYhGqQgoAJHnm5Mmqhx9ts8qxZLbdsh+T93mKvj+/yuai2Is+AJfLgZpdKPQN
+bCoZemRm+nghRvEP8aX/469wiz7SOLqUzxrTOtXV48wTU5LWLDCs1lF9ZdGHR9/r
+vj8unnEHIZiH3ZjN7OgaAoNHZE26Ywbmllc/a0vPw8iHdrLe7+Wtp4zXe2rcxhW7
+b+X1/yRCZ+Wg
+-----END CERTIFICATE-----
diff --git a/extras/hs-test/resources/cert/localhost.key b/extras/hs-test/resources/cert/localhost.key
new file mode 100644
index 00000000000..2d65db50900
--- /dev/null
+++ b/extras/hs-test/resources/cert/localhost.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy40rDzrrHPGIy
+hP24hOBQefEgKD5uUGgSUyJTCur4yB/r2PGtLlfipKwDmNArmZuFOgKh8evipu2j
+Yaxf4GHQmi7PGLddvPkqo5FWtVW8oAVJMcp+fwfs7OgkqtYD6Y7qjmjfXb9+rMpP
+N8WZ7cKbJwZpF3lf8GGaLqRmPiQg2j8qzcVynz8cIwBZP8BJVclA9GIagijY7Zcm
+z0HnTPrPoLMeyLJOTqPMfkUYA2H2eHeISkQPBeoFoiwCI5eM35UiWiLyiv9Kojn4
+BHx6MLrfKBjV13WtcRMgYm5VftsWOZ92lmHmbpj9mGgtd84JWtWxs33oG4mNRSAe
+ujf9AE5VAgMBAAECggEANwiZ/bdh2t2G0Ef9zoCCif+Z4OzAmCuAePK+gpG/TB41
+Q9eQMlkpjH5gtRKUKHWvVMNOAAhvK2FzhmoMH8rmDMkCUZAnCV2TwjxkACr1X3xT
+Y/s/cr8d7xPLL0ynXrjB0QNS3DT5Lr111/0ue3acAiN1Y2tnWc6YGFj1FsdTUg+O
+zRysrpNUp3LAK+MXIhAXMCGKOOLxpjeyrcnUokH0I8e06of1AfAHX8jTn65MG5Ex
+n9wBYPl+u2J3SjILHoqBKjcSoNILUfBN9mQGeXhoqCzwcnygDtOxIu9xgu2nCcJr
+C1R/WXoQ8Jr6wa1n0aEVXDJeOEK9kKXLTt2/I4HB2QKBgQDemLy+o2/tbFwlU2Xy
+8/tZa30kfLCAZ+kq+lE3Kkfqt3pPYzH+lfO7u/UWtavKRQRdsKsNKbpe/EdGq7c4
+YN3L1KG5JiIo3TxilUPilYacGHklfMMbEK7cs8Jebsl6rL7BgnKuqlXGY0HEEx8L
+XqIKN1RdzL04WLOiA8qDGwYp7wKBgQDNu3DECCTkTa+mZdNDRntoffkgyd0AnwPA
+PEf43BHORpKcfGwFIrf8QWRXcLdh72Yrc9o3D53GCq+NSYGPL2OiY+/3HoAy1mH1
+EBgS08qfkZBKr6+VGjWuVAlD2m2jW+AhGXMS+Lu4yzK3V+0EzlAu4WZVBUngg1//
+6ZtyvXLf+wKBgQCozmO0nvUutFJc7BYQXP5sHZvVo8mmVyb4NMSKdUH8ug/DTJKJ
+YuZnpG6FPlh9GEHrWyMc5Fw11FOpQGe+FZeeEC5k3ophOwWkLVZB6useTWDyEN9V
+Ex3IuXnZa2LX6VDwJyEZXIuX24XwUB/m22k/Hh6Y079bj8kKQJ2/NytBeQKBgQCZ
+RGMmJ8sUKqwJEyLoo8GcfvzyaHC03cI1nLMhuxGo0vq2ihsPWGYpD65pVhfIZkl/
+ZbfT/VZVC/DtGS3kNjHL8Rf8ykRHm18u6uaEYDQ73H3apjfwpK4JSaH9YuT7Jp87
+CXKpV5TCft8xp9d0FR+3TUSnYmE/WaBTTv335RuHsQKBgCFLyxzs0hM/MhCLHJ6b
+AqyNPz36Xcwsgit1Svhwm1IC6FqkSJl3cRKhp1AP5w6ktUfUGNpF/TYI3x2jCg/m
+c0nwmqi/3Cha64XKJcI4iT2+lyuE8jXovMdNiJEEKCDalpyYJbhzRaLsoSFSbiD1
+mFDl8/aNVaQKDDboSuj9AkKs
+-----END PRIVATE KEY-----
diff --git a/extras/hs-test/resources/envoy/envoy.log b/extras/hs-test/resources/envoy/envoy.log
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/extras/hs-test/resources/envoy/envoy.log
diff --git a/extras/hs-test/resources/envoy/proxy.yaml b/extras/hs-test/resources/envoy/proxy.yaml
new file mode 100644
index 00000000000..77da80d934d
--- /dev/null
+++ b/extras/hs-test/resources/envoy/proxy.yaml
@@ -0,0 +1,53 @@
+admin:
+ access_log_path: /tmp/envoy.log
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 8081
+static_resources:
+ listeners:
+ # define a reverse proxy on :10001 that always uses :80 as an origin.
+ - address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 555
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: proxy_service
+ http_filters:
+ - name: envoy.filters.http.router
+ clusters:
+ - name: proxy_service
+ connect_timeout: 0.25s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: proxy_service
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ # following address will be generated by Addresser during test run
+ address: {{.Server}}
+ port_value: 666
+bootstrap_extensions:
+ - name: envoy.extensions.vcl.vcl_socket_interface
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.vcl.v3alpha.VclSocketInterface
+default_socket_interface: "envoy.extensions.vcl.vcl_socket_interface"
diff --git a/extras/hs-test/resources/envoy/vcl.conf b/extras/hs-test/resources/envoy/vcl.conf
new file mode 100644
index 00000000000..164435a6ae5
--- /dev/null
+++ b/extras/hs-test/resources/envoy/vcl.conf
@@ -0,0 +1,7 @@
+vcl {
+ rx-fifo-size 400000
+ tx-fifo-size 400000
+ app-scope-global
+ use-mq-eventfd
+ app-socket-api /tmp/vpp-envoy/var/run/app_ns_sockets/default
+}
diff --git a/extras/hs-test/resources/nginx/html/index.html b/extras/hs-test/resources/nginx/html/index.html
new file mode 100644
index 00000000000..6b7c97d7542
--- /dev/null
+++ b/extras/hs-test/resources/nginx/html/index.html
@@ -0,0 +1,6 @@
+<http>
+ <title>nginx docker with quic</title>
+<body>
+ <p>Greetings!</p>
+</body>
+</http>
diff --git a/extras/hs-test/resources/nginx/nginx.conf b/extras/hs-test/resources/nginx/nginx.conf
new file mode 100644
index 00000000000..99073aab1ab
--- /dev/null
+++ b/extras/hs-test/resources/nginx/nginx.conf
@@ -0,0 +1,30 @@
+master_process on;
+worker_rlimit_nofile 10240;
+worker_processes 2;
+daemon off;
+
+events {
+ use epoll;
+ worker_connections 10240;
+ accept_mutex off;
+ multi_accept off;
+}
+
+http {
+ keepalive_timeout 300s;
+ keepalive_requests 1000000;
+ sendfile on;
+ server {
+ listen 80;
+ root /usr/share/nginx;
+ index index.html index.htm;
+ location /return_ok
+ {
+ return 200 '';
+ }
+ location /64B.json
+ {
+ return 200 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
+ }
+ }
+}
diff --git a/extras/hs-test/resources/nginx/nginx_http3.conf b/extras/hs-test/resources/nginx/nginx_http3.conf
new file mode 100644
index 00000000000..2a01f714111
--- /dev/null
+++ b/extras/hs-test/resources/nginx/nginx_http3.conf
@@ -0,0 +1,26 @@
+master_process on;
+worker_processes 2;
+daemon off;
+
+events {
+ use epoll;
+ accept_mutex off;
+ multi_accept off;
+}
+
+http {
+ quic_gso on;
+ quic_retry on;
+
+ access_log logs/access.log;
+ keepalive_timeout 300s;
+ sendfile on;
+ server {
+ listen 0.0.0.0:8443 quic;
+ #listen 0.0.0.0:8443 ssl;
+ root /usr/share/nginx;
+ ssl_certificate /etc/nginx/ssl/localhost.crt;
+ ssl_certificate_key /etc/nginx/ssl/localhost.key;
+ index index.html index.htm;
+ }
+}
diff --git a/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf b/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf
new file mode 100644
index 00000000000..56debf5c290
--- /dev/null
+++ b/extras/hs-test/resources/nginx/nginx_proxy_mirroring.conf
@@ -0,0 +1,84 @@
+master_process on;
+worker_processes 4;
+worker_rlimit_nofile 102400;
+daemon off;
+
+error_log /tmp/nginx/error.log;
+
+events {
+ use epoll;
+ worker_connections 102400;
+ accept_mutex off;
+}
+
+http {
+ include mime.types;
+ default_type application/octet-stream;
+
+ access_log off;
+
+ keepalive_timeout 300;
+ keepalive_requests 1000000;
+
+ proxy_connect_timeout 300;
+ large_client_header_buffers 4 512k;
+ client_max_body_size 3000m;
+ client_header_buffer_size 2048m;
+ client_body_buffer_size 1024m;
+ proxy_buffers 16 10240k;
+ proxy_buffer_size 10240k;
+
+ gzip on;
+
+ upstream bk {
+ server {{.Server}}:8091;
+ keepalive 30000;
+ }
+ upstream bk1 {
+ server {{.Server}}:8092;
+ keepalive 30000;
+ }
+ upstream bk2 {
+ server {{.Server}}:8093;
+ keepalive 30000;
+ }
+
+ server {
+ listen 80;
+ server_name {{.Proxy}};
+
+ server_tokens off;
+
+ proxy_redirect off;
+
+ location / {
+ root html;
+ index index.html index.htm;
+ proxy_pass http://bk;
+ proxy_set_header Connection "";
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header Host $host:$server_port;
+ chunked_transfer_encoding on;
+ proxy_http_version 1.1;
+ mirror /mimic1;
+ mirror /mimic2;
+ mirror_request_body on;
+ }
+ location /mimic1 {
+ proxy_pass http://bk1$request_uri;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header Connection "";
+ chunked_transfer_encoding on;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host:$server_port;
+ }
+ location /mimic2 {
+ proxy_pass http://bk2$request_uri;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header Host $host:$server_port;
+ proxy_set_header Connection "";
+ proxy_http_version 1.1;
+ chunked_transfer_encoding on;
+ }
+ }
+}
diff --git a/extras/hs-test/resources/nginx/nginx_server_mirroring.conf b/extras/hs-test/resources/nginx/nginx_server_mirroring.conf
new file mode 100644
index 00000000000..4056801ea13
--- /dev/null
+++ b/extras/hs-test/resources/nginx/nginx_server_mirroring.conf
@@ -0,0 +1,32 @@
+master_process on;
+worker_rlimit_nofile 10240;
+worker_processes 2;
+daemon off;
+
+events {
+ use epoll;
+ worker_connections 10240;
+ accept_mutex off;
+ multi_accept off;
+}
+
+http {
+ keepalive_timeout 300s;
+ keepalive_requests 1000000;
+ sendfile on;
+ server {
+ listen 8091;
+ listen 8092;
+ listen 8093;
+ root /usr/share/nginx;
+ index index.html index.htm;
+ location /return_ok
+ {
+ return 200 '';
+ }
+ location /64B.json
+ {
+ return 200 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
+ }
+ }
+}
diff --git a/extras/hs-test/resources/nginx/vcl.conf b/extras/hs-test/resources/nginx/vcl.conf
new file mode 100644
index 00000000000..cfcd5d2e959
--- /dev/null
+++ b/extras/hs-test/resources/nginx/vcl.conf
@@ -0,0 +1,11 @@
+vcl {
+ heapsize 64M
+ segment-size 4000000000
+ add-segment-size 4000000000
+ rx-fifo-size 4000000
+ tx-fifo-size 4000000
+ event-queue-size 100000
+
+ use-mq-eventfd
+ app-socket-api /tmp/nginx/var/run/app_ns_sockets/default
+}
diff --git a/extras/hs-test/script/build_boringssl.sh b/extras/hs-test/script/build_boringssl.sh
new file mode 100755
index 00000000000..441878a77ca
--- /dev/null
+++ b/extras/hs-test/script/build_boringssl.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+cd boringssl
+cmake -GNinja -B build
+ninja -C build
diff --git a/extras/hs-test/script/build_curl.sh b/extras/hs-test/script/build_curl.sh
new file mode 100755
index 00000000000..e4258946f9e
--- /dev/null
+++ b/extras/hs-test/script/build_curl.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+wget https://github.com/stunnel/static-curl/releases/download/8.5.0/curl-static-amd64-8.5.0.tar.xz
+tar -xvf ./curl-static-amd64-8.5.0.tar.xz
+cp curl /usr/bin/curl \ No newline at end of file
diff --git a/extras/hs-test/script/build_hst.sh b/extras/hs-test/script/build_hst.sh
new file mode 100755
index 00000000000..3b4bc28e275
--- /dev/null
+++ b/extras/hs-test/script/build_hst.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+if [ $(lsb_release -is) != Ubuntu ]; then
+ echo "Host stack test framework is supported only on Ubuntu"
+ exit 1
+fi
+
+if [ -z $(which ab) ]; then
+ echo "Host stack test framework requires apache2-utils to be installed"
+ echo "It is recommended to run 'sudo make install-dep'"
+ exit 1
+fi
+
+if [ -z $(which wrk) ]; then
+ echo "Host stack test framework requires wrk to be installed"
+ echo "It is recommended to run 'sudo make install-dep'"
+ exit 1
+fi
+
+export VPP_WS=../..
+
+if [ "$1" == "debug" ]; then
+ VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp_debug-native/vpp
+elif [ "$1" == "gcov" ]; then
+ VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp_gcov-native/vpp
+else
+ VPP_BUILD_ROOT=${VPP_WS}/build-root/build-vpp-native/vpp
+fi
+echo "Taking build objects from ${VPP_BUILD_ROOT}"
+
+if [ -z "$UBUNTU_VERSION" ] ; then
+ export UBUNTU_VERSION=$(lsb_release -rs)
+fi
+echo "Ubuntu version is set to ${UBUNTU_VERSION}"
+
+export HST_LDPRELOAD=${VPP_BUILD_ROOT}/lib/x86_64-linux-gnu/libvcl_ldpreload.so
+echo "HST_LDPRELOAD is set to ${HST_LDPRELOAD}"
+
+export PATH=${VPP_BUILD_ROOT}/bin:$PATH
+
+bin=vpp-data/bin
+lib=vpp-data/lib
+
+mkdir -p ${bin} ${lib} || true
+rm -rf vpp-data/bin/* || true
+rm -rf vpp-data/lib/* || true
+
+cp ${VPP_BUILD_ROOT}/bin/* ${bin}
+res+=$?
+cp -r ${VPP_BUILD_ROOT}/lib/x86_64-linux-gnu/* ${lib}
+res+=$?
+if [ $res -ne 0 ]; then
+ echo "Failed to copy VPP files. Is VPP built? Try running 'make build' in VPP directory."
+ exit 1
+fi
+
+docker_build () {
+ tag=$1
+ dockername=$2
+ docker build --build-arg UBUNTU_VERSION \
+ --build-arg http_proxy=$HTTP_PROXY \
+ --build-arg https_proxy=$HTTP_PROXY \
+ --build-arg HTTP_PROXY=$HTTP_PROXY \
+ --build-arg HTTPS_PROXY=$HTTP_PROXY \
+ -t $tag -f docker/Dockerfile.$dockername .
+}
+
+docker_build hs-test/vpp vpp
+docker_build hs-test/nginx-ldp nginx
+docker_build hs-test/nginx-server nginx-server
+docker_build hs-test/build build
+if [ "$HST_EXTENDED_TESTS" = true ] ; then
+ docker_build hs-test/nginx-http3 nginx-http3
+ docker_build hs-test/curl curl
+fi
+
+# cleanup detached images
+images=$(docker images --filter "dangling=true" -q --no-trunc)
+if [ "$images" != "" ]; then
+ docker rmi $images
+fi
diff --git a/extras/hs-test/script/build_nginx.sh b/extras/hs-test/script/build_nginx.sh
new file mode 100755
index 00000000000..69d366aab0e
--- /dev/null
+++ b/extras/hs-test/script/build_nginx.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+cd nginx
+./auto/configure --with-debug --with-http_v3_module --with-cc-opt="-I../boringssl/include" --with-ld-opt="-L../boringssl/build/ssl -L../boringssl/build/crypto" --without-http_rewrite_module --without-http_gzip_module
+make
+make install
diff --git a/extras/hs-test/script/compress.sh b/extras/hs-test/script/compress.sh
new file mode 100644
index 00000000000..1f0205c1efb
--- /dev/null
+++ b/extras/hs-test/script/compress.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+# if failed-summary.log is not empty, exit status = 1
+if [ -s "${HS_ROOT}/summary/failed-summary.log" ]
+then
+ if [ -n "${WORKSPACE}" ]
+ then
+ echo -n "Copying docker logs..."
+ dirs=$(jq -r '.[0] | .SpecReports[] | select(.State == "failed") | .LeafNodeText' ${HS_ROOT}/summary/report.json)
+ for dirName in $dirs; do
+ logDir=/tmp/hs-test/$dirName
+ if [ -d "$logDir" ]; then
+ mkdir -p ${WORKSPACE}/archives/summary
+ cp -r $logDir ${WORKSPACE}/archives/summary/
+ fi
+ done
+ echo "Done."
+
+ echo -n "Copying failed test logs into build log archive directory (${WORKSPACE}/archives)... "
+ mkdir -p ${WORKSPACE}/archives/summary
+ cp -a ${HS_ROOT}/summary/* ${WORKSPACE}/archives/summary
+ echo "Done."
+
+ echo -n "Compressing files in ${WORKSPACE}/archives from test runs... "
+ cd ${WORKSPACE}/archives
+ find . -type f \( -name "*.json" -o -name "*.log" \) -exec gzip {} \;
+ echo "Done."
+
+ else
+ echo "Not compressing files in temporary directories from test runs."
+ fi
+ exit 1
+fi
diff --git a/extras/hs-test/script/nginx_ldp.sh b/extras/hs-test/script/nginx_ldp.sh
new file mode 100755
index 00000000000..4a22e14aaf7
--- /dev/null
+++ b/extras/hs-test/script/nginx_ldp.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+LD_PRELOAD=$LDP $@ 2>&1 > /proc/1/fd/1
diff --git a/extras/hs-test/suite_nginx_test.go b/extras/hs-test/suite_nginx_test.go
new file mode 100644
index 00000000000..4c6e9dbb309
--- /dev/null
+++ b/extras/hs-test/suite_nginx_test.go
@@ -0,0 +1,132 @@
+package main
+
+import (
+ "reflect"
+ "runtime"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+// These correspond to names used in yaml config
+const (
+ vppProxyContainerName = "vpp-proxy"
+ nginxProxyContainerName = "nginx-proxy"
+ nginxServerContainerName = "nginx-server"
+ mirroringClientInterfaceName = "hstcln"
+ mirroringServerInterfaceName = "hstsrv"
+)
+
+var nginxTests = []func(s *NginxSuite){}
+var nginxSoloTests = []func(s *NginxSuite){}
+
+type NginxSuite struct {
+ HstSuite
+}
+
+func registerNginxTests(tests ...func(s *NginxSuite)) {
+ nginxTests = append(nginxTests, tests...)
+}
+func registerNginxSoloTests(tests ...func(s *NginxSuite)) {
+ nginxSoloTests = append(nginxSoloTests, tests...)
+}
+
+func (s *NginxSuite) SetupSuite() {
+ s.HstSuite.SetupSuite()
+ s.loadNetworkTopology("2taps")
+ s.loadContainerTopology("nginxProxyAndServer")
+}
+
+func (s *NginxSuite) SetupTest() {
+ s.HstSuite.SetupTest()
+
+ // Setup test conditions
+ var sessionConfig Stanza
+ sessionConfig.
+ newStanza("session").
+ append("enable").
+ append("use-app-socket-api").close()
+
+ // ... for proxy
+ vppProxyContainer := s.getContainerByName(vppProxyContainerName)
+ proxyVpp, _ := vppProxyContainer.newVppInstance(vppProxyContainer.allocatedCpus, sessionConfig)
+ s.assertNil(proxyVpp.start())
+
+ clientInterface := s.getInterfaceByName(mirroringClientInterfaceName)
+ s.assertNil(proxyVpp.createTap(clientInterface, 1))
+
+ serverInterface := s.getInterfaceByName(mirroringServerInterfaceName)
+ s.assertNil(proxyVpp.createTap(serverInterface, 2))
+
+ nginxContainer := s.getTransientContainerByName(nginxProxyContainerName)
+ nginxContainer.create()
+
+ values := struct {
+ Proxy string
+ Server string
+ }{
+ Proxy: clientInterface.peer.ip4AddressString(),
+ Server: serverInterface.ip4AddressString(),
+ }
+ nginxContainer.createConfig(
+ "/nginx.conf",
+ "./resources/nginx/nginx_proxy_mirroring.conf",
+ values,
+ )
+ s.assertNil(nginxContainer.start())
+
+ proxyVpp.waitForApp("nginx-", 5)
+}
+
+var _ = Describe("NginxSuite", Ordered, ContinueOnFailure, func() {
+ var s NginxSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+ for _, test := range nginxTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
+
+var _ = Describe("NginxSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ var s NginxSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range nginxSoloTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, Label("SOLO"), func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
diff --git a/extras/hs-test/suite_no_topo_test.go b/extras/hs-test/suite_no_topo_test.go
new file mode 100644
index 00000000000..260fc1d681d
--- /dev/null
+++ b/extras/hs-test/suite_no_topo_test.go
@@ -0,0 +1,108 @@
+package main
+
+import (
+ "reflect"
+ "runtime"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+const (
+ singleTopoContainerVpp = "vpp"
+ singleTopoContainerNginx = "nginx"
+ tapInterfaceName = "htaphost"
+)
+
+var noTopoTests = []func(s *NoTopoSuite){}
+var noTopoSoloTests = []func(s *NoTopoSuite){}
+
+type NoTopoSuite struct {
+ HstSuite
+}
+
+func registerNoTopoTests(tests ...func(s *NoTopoSuite)) {
+ noTopoTests = append(noTopoTests, tests...)
+}
+func registerNoTopoSoloTests(tests ...func(s *NoTopoSuite)) {
+ noTopoSoloTests = append(noTopoSoloTests, tests...)
+}
+
+func (s *NoTopoSuite) SetupSuite() {
+ s.HstSuite.SetupSuite()
+ s.loadNetworkTopology("tap")
+ s.loadContainerTopology("single")
+}
+
+func (s *NoTopoSuite) SetupTest() {
+ s.HstSuite.SetupTest()
+
+ // Setup test conditions
+ var sessionConfig Stanza
+ sessionConfig.
+ newStanza("session").
+ append("enable").
+ append("use-app-socket-api").close()
+
+ container := s.getContainerByName(singleTopoContainerVpp)
+ vpp, _ := container.newVppInstance(container.allocatedCpus, sessionConfig)
+ s.assertNil(vpp.start())
+
+ tapInterface := s.getInterfaceByName(tapInterfaceName)
+
+ s.assertNil(vpp.createTap(tapInterface), "failed to create tap interface")
+}
+
+var _ = Describe("NoTopoSuite", Ordered, ContinueOnFailure, func() {
+ var s NoTopoSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range noTopoTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
+
+var _ = Describe("NoTopoSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ var s NoTopoSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range noTopoSoloTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, Label("SOLO"), func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
diff --git a/extras/hs-test/suite_ns_test.go b/extras/hs-test/suite_ns_test.go
new file mode 100644
index 00000000000..7bdb90b42ae
--- /dev/null
+++ b/extras/hs-test/suite_ns_test.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+// These correspond to names used in yaml config
+const (
+ clientInterface = "hclnvpp"
+ serverInterface = "hsrvvpp"
+)
+
+var nsTests = []func(s *NsSuite){}
+var nsSoloTests = []func(s *NsSuite){}
+
+type NsSuite struct {
+ HstSuite
+}
+
+func registerNsTests(tests ...func(s *NsSuite)) {
+ nsTests = append(nsTests, tests...)
+}
+func registerNsSoloTests(tests ...func(s *NsSuite)) {
+ nsSoloTests = append(nsSoloTests, tests...)
+}
+
+func (s *NsSuite) SetupSuite() {
+ s.HstSuite.SetupSuite()
+ s.configureNetworkTopology("ns")
+ s.loadContainerTopology("ns")
+}
+
+func (s *NsSuite) SetupTest() {
+ s.HstSuite.SetupTest()
+
+ // Setup test conditions
+ var sessionConfig Stanza
+ sessionConfig.
+ newStanza("session").
+ append("enable").
+ append("use-app-socket-api").
+ append("evt_qs_memfd_seg").
+ append("event-queue-length 100000").close()
+
+ container := s.getContainerByName("vpp")
+ vpp, _ := container.newVppInstance(container.allocatedCpus, sessionConfig)
+ s.assertNil(vpp.start())
+
+ idx, err := vpp.createAfPacket(s.getInterfaceByName(serverInterface))
+ s.assertNil(err, fmt.Sprint(err))
+ s.assertNotEqual(0, idx)
+
+ idx, err = vpp.createAfPacket(s.getInterfaceByName(clientInterface))
+ s.assertNil(err, fmt.Sprint(err))
+ s.assertNotEqual(0, idx)
+
+ container.exec("chmod 777 -R %s", container.getContainerWorkDir())
+}
+
+var _ = Describe("NsSuite", Ordered, ContinueOnFailure, func() {
+ var s NsSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range nsTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
+
+var _ = Describe("NsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ var s NsSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range nsSoloTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, Label("SOLO"), func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
diff --git a/extras/hs-test/suite_tap_test.go b/extras/hs-test/suite_tap_test.go
new file mode 100644
index 00000000000..cb0653304c3
--- /dev/null
+++ b/extras/hs-test/suite_tap_test.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "reflect"
+ "runtime"
+ "strings"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+type TapSuite struct {
+ HstSuite
+}
+
+var tapTests = []func(s *TapSuite){}
+var tapSoloTests = []func(s *TapSuite){}
+
+func registerTapTests(tests ...func(s *TapSuite)) {
+ tapTests = append(tapTests, tests...)
+}
+func registerTapSoloTests(tests ...func(s *TapSuite)) {
+ tapSoloTests = append(tapSoloTests, tests...)
+}
+
+func (s *TapSuite) SetupSuite() {
+ time.Sleep(1 * time.Second)
+ s.HstSuite.SetupSuite()
+ s.configureNetworkTopology("tap")
+}
+
+var _ = Describe("TapSuite", Ordered, ContinueOnFailure, func() {
+ var s TapSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range tapTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
+
+var _ = Describe("TapSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ var s TapSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ for _, test := range tapSoloTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, Label("SOLO"), func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
diff --git a/extras/hs-test/suite_veth_test.go b/extras/hs-test/suite_veth_test.go
new file mode 100644
index 00000000000..13ef5ef268d
--- /dev/null
+++ b/extras/hs-test/suite_veth_test.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+ "strings"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+)
+
+// These correspond to names used in yaml config
+const (
+ serverInterfaceName = "srv"
+ clientInterfaceName = "cln"
+)
+
+var vethTests = []func(s *VethsSuite){}
+var vethSoloTests = []func(s *VethsSuite){}
+
+type VethsSuite struct {
+ HstSuite
+}
+
+func registerVethTests(tests ...func(s *VethsSuite)) {
+ vethTests = append(vethTests, tests...)
+}
+func registerSoloVethTests(tests ...func(s *VethsSuite)) {
+ vethSoloTests = append(vethSoloTests, tests...)
+}
+
+func (s *VethsSuite) SetupSuite() {
+ time.Sleep(1 * time.Second)
+ s.HstSuite.SetupSuite()
+ s.configureNetworkTopology("2peerVeth")
+ s.loadContainerTopology("2peerVeth")
+}
+
+func (s *VethsSuite) SetupTest() {
+ s.HstSuite.SetupTest()
+
+ // Setup test conditions
+ var sessionConfig Stanza
+ sessionConfig.
+ newStanza("session").
+ append("enable").
+ append("use-app-socket-api").close()
+
+ // ... For server
+ serverContainer := s.getContainerByName("server-vpp")
+
+ serverVpp, err := serverContainer.newVppInstance(serverContainer.allocatedCpus, sessionConfig)
+ s.assertNotNil(serverVpp, fmt.Sprint(err))
+
+ s.setupServerVpp()
+
+ // ... For client
+ clientContainer := s.getContainerByName("client-vpp")
+
+ clientVpp, err := clientContainer.newVppInstance(clientContainer.allocatedCpus, sessionConfig)
+ s.assertNotNil(clientVpp, fmt.Sprint(err))
+
+ s.setupClientVpp()
+}
+
+func (s *VethsSuite) setupServerVpp() {
+ serverVpp := s.getContainerByName("server-vpp").vppInstance
+ s.assertNil(serverVpp.start())
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ idx, err := serverVpp.createAfPacket(serverVeth)
+ s.assertNil(err, fmt.Sprint(err))
+ s.assertNotEqual(0, idx)
+}
+
+func (s *VethsSuite) setupClientVpp() {
+ clientVpp := s.getContainerByName("client-vpp").vppInstance
+ s.assertNil(clientVpp.start())
+
+ clientVeth := s.getInterfaceByName(clientInterfaceName)
+ idx, err := clientVpp.createAfPacket(clientVeth)
+ s.assertNil(err, fmt.Sprint(err))
+ s.assertNotEqual(0, idx)
+}
+
+var _ = Describe("VethsSuite", Ordered, ContinueOnFailure, func() {
+ var s VethsSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ // https://onsi.github.io/ginkgo/#dynamically-generating-specs
+ for _, test := range vethTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
+
+var _ = Describe("VethsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+ var s VethsSuite
+ BeforeAll(func() {
+ s.SetupSuite()
+ })
+ BeforeEach(func() {
+ s.SetupTest()
+ })
+ AfterAll(func() {
+ s.TearDownSuite()
+ })
+ AfterEach(func() {
+ s.TearDownTest()
+ })
+
+ // https://onsi.github.io/ginkgo/#dynamically-generating-specs
+ for _, test := range vethSoloTests {
+ test := test
+ pc := reflect.ValueOf(test).Pointer()
+ funcValue := runtime.FuncForPC(pc)
+ testName := strings.Split(funcValue.Name(), ".")[2]
+ It(testName, Label("SOLO"), func(ctx SpecContext) {
+ s.log(testName + ": BEGIN")
+ test(&s)
+ }, SpecTimeout(suiteTimeout))
+ }
+})
diff --git a/extras/hs-test/test b/extras/hs-test/test
new file mode 100644
index 00000000000..398e2b39edb
--- /dev/null
+++ b/extras/hs-test/test
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+source vars
+
+args=
+single_test=0
+persist_set=0
+unconfigure_set=0
+debug_set=0
+vppsrc=
+ginkgo_args=
+parallel=
+
+for i in "$@"
+do
+case "${i}" in
+ --persist=*)
+ persist="${i#*=}"
+ if [ $persist = "true" ]; then
+ args="$args -persist"
+ persist_set=1
+ fi
+ ;;
+ --debug=*)
+ debug="${i#*=}"
+ if [ $debug = "true" ]; then
+ args="$args -debug"
+ debug_set=1
+ fi
+ ;;
+ --verbose=*)
+ verbose="${i#*=}"
+ if [ $verbose = "true" ]; then
+ args="$args -verbose"
+ fi
+ ;;
+ --unconfigure=*)
+ unconfigure="${i#*=}"
+ if [ $unconfigure = "true" ]; then
+ args="$args -unconfigure"
+ unconfigure_set=1
+ fi
+ ;;
+ --cpus=*)
+ args="$args -cpus ${i#*=}"
+ ;;
+ --vppsrc=*)
+ args="$args -vppsrc ${i#*=}"
+ ;;
+ --test=*)
+ tc_name="${i#*=}"
+ if [ $tc_name != "all" ]; then
+ single_test=1
+ ginkgo_args="$ginkgo_args --focus $tc_name -vv"
+ args="$args -verbose"
+ else
+ ginkgo_args="$ginkgo_args -v"
+ fi
+ ;;
+ --parallel=*)
+ ginkgo_args="$ginkgo_args -procs=${i#*=}"
+ ;;
+ --repeat=*)
+ ginkgo_args="$ginkgo_args --repeat=${i#*=}"
+ ;;
+esac
+done
+
+if [ $single_test -eq 0 ] && [ $persist_set -eq 1 ]; then
+ echo "persist flag is not supported while running all tests!"
+ exit 1
+fi
+
+if [ $unconfigure_set -eq 1 ] && [ $single_test -eq 0 ]; then
+ echo "a single test has to be specified when unconfigure is set"
+ exit 1
+fi
+
+if [ $persist_set -eq 1 ] && [ $unconfigure_set -eq 1 ]; then
+ echo "setting persist flag and unconfigure flag is not allowed"
+ exit 1
+fi
+
+if [ $single_test -eq 0 ] && [ $debug_set -eq 1 ]; then
+ echo "VPP debug flag is not supperted while running all tests!"
+ exit 1
+fi
+
+mkdir -p summary
+
+sudo -E go run github.com/onsi/ginkgo/v2/ginkgo --no-color --trace --json-report=summary/report.json $ginkgo_args -- $args
+
+jq -r '.[0] | .SpecReports[] | select((.State == "failed") or (.State == "timedout") or (.State == "panicked")) | select(.Failure != null) | "TestName: \(.LeafNodeText)\nSuite:\n\(.Failure.Location.FileName)\nMessage:\n\(.Failure.Message)\n Full Stack Trace:\n\(.Failure.Location.FullStackTrace)\n"' summary/report.json > summary/failed-summary.log \
+ && echo "Summary generated -> summary/failed-summary.log" \ No newline at end of file
diff --git a/extras/hs-test/tools/http_server/http_server.go b/extras/hs-test/tools/http_server/http_server.go
new file mode 100644
index 00000000000..d2ab3851643
--- /dev/null
+++ b/extras/hs-test/tools/http_server/http_server.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+)
+
+func main() {
+ if len(os.Args) < 3 {
+ fmt.Println("arg expected")
+ os.Exit(1)
+ }
+
+ http.HandleFunc("/httpTestFile", func(w http.ResponseWriter, r *http.Request) {
+ file, _ := os.Open("httpTestFile" + os.Args[2])
+ defer file.Close()
+ io.Copy(w, file)
+ })
+ err := http.ListenAndServe(os.Args[1], nil)
+ if err != nil {
+ fmt.Printf("%v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/extras/hs-test/topo-containers/2peerVeth.yaml b/extras/hs-test/topo-containers/2peerVeth.yaml
new file mode 100644
index 00000000000..e1591fb9019
--- /dev/null
+++ b/extras/hs-test/topo-containers/2peerVeth.yaml
@@ -0,0 +1,25 @@
+---
+volumes:
+ - volume: &server-vol
+ host-dir: "$HST_VOLUME_DIR/server-share"
+ container-dir: "/tmp/server-share"
+ is-default-work-dir: true
+ - volume: &client-vol
+ host-dir: "$HST_VOLUME_DIR/client-share"
+ container-dir: "/tmp/client-share"
+ is-default-work-dir: true
+
+containers:
+ - name: "server-vpp"
+ volumes:
+ - <<: *server-vol
+ - name: "client-vpp"
+ volumes:
+ - <<: *client-vol
+ - name: "server-app"
+ volumes:
+ - <<: *server-vol
+ - name: "client-app"
+ volumes:
+ - <<: *client-vol
+
diff --git a/extras/hs-test/topo-containers/nginxProxyAndServer.yaml b/extras/hs-test/topo-containers/nginxProxyAndServer.yaml
new file mode 100644
index 00000000000..cc6b780bafc
--- /dev/null
+++ b/extras/hs-test/topo-containers/nginxProxyAndServer.yaml
@@ -0,0 +1,20 @@
+---
+volumes:
+ - volume: &shared-vol-proxy
+ host-dir: "$HST_VOLUME_DIR/shared-vol-proxy"
+
+containers:
+ - name: "vpp-proxy"
+ volumes:
+ - <<: *shared-vol-proxy
+ container-dir: "/tmp/vpp"
+ is-default-work-dir: true
+ - name: "nginx-proxy"
+ volumes:
+ - <<: *shared-vol-proxy
+ container-dir: "/tmp/nginx"
+ is-default-work-dir: true
+ image: "hs-test/nginx-ldp"
+ is-optional: true
+ - name: "nginx-server"
+ image: "hs-test/nginx-server"
diff --git a/extras/hs-test/topo-containers/ns.yaml b/extras/hs-test/topo-containers/ns.yaml
new file mode 100644
index 00000000000..2298ad232c2
--- /dev/null
+++ b/extras/hs-test/topo-containers/ns.yaml
@@ -0,0 +1,27 @@
+---
+volumes:
+ - volume: &shared-vol
+ host-dir: "$HST_VOLUME_DIR/shared-vol"
+
+# $HST_DIR will be replaced during runtime by path to hs-test directory
+containers:
+ - name: "vpp"
+ volumes:
+ - <<: *shared-vol
+ container-dir: "/tmp/vpp"
+ is-default-work-dir: true
+ - name: "envoy"
+ volumes:
+ - <<: *shared-vol
+ container-dir: "/tmp/vpp-envoy"
+ is-default-work-dir: true
+ - host-dir: "$HST_DIR/resources/envoy"
+ container-dir: "/tmp"
+ vars:
+ - name: "ENVOY_UID"
+ value: "0"
+ - name: "VCL_CONFIG"
+ value: "/tmp/vcl.conf"
+ image: "envoyproxy/envoy-contrib:v1.21-latest"
+ extra-args: "--concurrency 2 -c /etc/envoy/envoy.yaml"
+ is-optional: true
diff --git a/extras/hs-test/topo-containers/single.yaml b/extras/hs-test/topo-containers/single.yaml
new file mode 100644
index 00000000000..b6970c517bd
--- /dev/null
+++ b/extras/hs-test/topo-containers/single.yaml
@@ -0,0 +1,47 @@
+---
+volumes:
+ - volume: &shared-vol
+ host-dir: "$HST_VOLUME_DIR/shared-vol"
+
+containers:
+ - name: "vpp"
+ volumes:
+ - <<: *shared-vol
+ container-dir: "/tmp/vpp"
+ is-default-work-dir: true
+
+ - name: "nginx"
+ volumes:
+ - <<: *shared-vol
+ container-dir: "/tmp/nginx"
+ is-default-work-dir: true
+ image: "hs-test/nginx-ldp"
+ is-optional: true
+
+ - name: "nginx-http3"
+ volumes:
+ - <<: *shared-vol
+ container-dir: "/tmp/nginx"
+ is-default-work-dir: true
+ - host-dir: $HST_DIR/resources/cert
+ container-dir: "/etc/nginx/ssl"
+ image: "hs-test/nginx-http3"
+ is-optional: true
+
+ - name: "ab"
+ image: "jordi/ab"
+ is-optional: true
+ run-detached: false
+
+ - name: "wrk"
+ image: "skandyla/wrk"
+ is-optional: true
+ run-detached: false
+
+ - name: "curl"
+ vars:
+ - name: LD_LIBRARY_PATH
+ value: "/usr/local/lib"
+ image: "hs-test/curl"
+ is-optional: true
+ run-detached: false \ No newline at end of file
diff --git a/extras/hs-test/topo-network/2peerVeth.yaml b/extras/hs-test/topo-network/2peerVeth.yaml
new file mode 100644
index 00000000000..f991d8b3701
--- /dev/null
+++ b/extras/hs-test/topo-network/2peerVeth.yaml
@@ -0,0 +1,25 @@
+---
+devices:
+ - name: "hsns"
+ type: "netns"
+
+ - name: "srv"
+ type: "veth"
+ preset-hw-address: "00:00:5e:00:53:01"
+ peer:
+ name: "srv_veth"
+ netns: "hsns"
+
+ - name: "cln"
+ type: "veth"
+ peer:
+ name: "cln_veth"
+ netns: "hsns"
+
+ - name: "br"
+ type: "bridge"
+ netns: "hsns"
+ interfaces:
+ - srv_veth
+ - cln_veth
+
diff --git a/extras/hs-test/topo-network/2taps.yaml b/extras/hs-test/topo-network/2taps.yaml
new file mode 100644
index 00000000000..f5dd8e2adda
--- /dev/null
+++ b/extras/hs-test/topo-network/2taps.yaml
@@ -0,0 +1,18 @@
+---
+devices:
+ - name: "hstcln"
+ type: "tap"
+ ip4:
+ network: 1
+ peer:
+ name: ""
+ ip4:
+ network: 1
+ - name: "hstsrv"
+ type: "tap"
+ ip4:
+ network: 2
+ peer:
+ name: ""
+ ip4:
+ network: 2
diff --git a/extras/hs-test/topo-network/ns.yaml b/extras/hs-test/topo-network/ns.yaml
new file mode 100644
index 00000000000..018c329f77e
--- /dev/null
+++ b/extras/hs-test/topo-network/ns.yaml
@@ -0,0 +1,23 @@
+---
+devices:
+ - name: "cln"
+ type: "netns"
+
+ - name: "srv"
+ type: "netns"
+
+ - name: "hclnvpp"
+ type: "veth"
+ peer:
+ name: "cln"
+ netns: "cln"
+ ip4:
+ network: 1
+
+ - name: "hsrvvpp"
+ type: "veth"
+ peer:
+ name: "srv"
+ netns: "srv"
+ ip4:
+ network: 2
diff --git a/extras/hs-test/topo-network/tap.yaml b/extras/hs-test/topo-network/tap.yaml
new file mode 100644
index 00000000000..acf14958ba3
--- /dev/null
+++ b/extras/hs-test/topo-network/tap.yaml
@@ -0,0 +1,10 @@
+---
+devices:
+ - name: "htaphost"
+ type: "tap"
+ ip4:
+ network: 1
+ peer:
+ name: ""
+ ip4:
+ network: 1
diff --git a/extras/hs-test/topo.go b/extras/hs-test/topo.go
new file mode 100644
index 00000000000..6cb294511b3
--- /dev/null
+++ b/extras/hs-test/topo.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "fmt"
+)
+
+type NetDevConfig map[string]interface{}
+type ContainerConfig map[string]interface{}
+type VolumeConfig map[string]interface{}
+
+type YamlTopology struct {
+ Devices []NetDevConfig `yaml:"devices"`
+ Containers []ContainerConfig `yaml:"containers"`
+ Volumes []VolumeConfig `yaml:"volumes"`
+}
+
+func addAddress(device, address, ns string) error {
+ c := []string{"ip", "addr", "add", address, "dev", device}
+ cmd := appendNetns(c, ns)
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("failed to set ip address for %s: %v", device, err)
+ }
+ return nil
+}
diff --git a/extras/hs-test/utils.go b/extras/hs-test/utils.go
new file mode 100644
index 00000000000..d250dc64519
--- /dev/null
+++ b/extras/hs-test/utils.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+)
+
+const networkTopologyDir string = "topo-network/"
+const containerTopologyDir string = "topo-containers/"
+
+type Stanza struct {
+ content string
+ pad int
+}
+
+type ActionResult struct {
+ Err error
+ Desc string
+ ErrOutput string
+ StdOutput string
+}
+
+type JsonResult struct {
+ Code int
+ Desc string
+ ErrOutput string
+ StdOutput string
+}
+
+func assertFileSize(f1, f2 string) error {
+ fi1, err := os.Stat(f1)
+ if err != nil {
+ return err
+ }
+
+ fi2, err1 := os.Stat(f2)
+ if err1 != nil {
+ return err1
+ }
+
+ if fi1.Size() != fi2.Size() {
+ return fmt.Errorf("file sizes differ (%d vs %d)", fi1.Size(), fi2.Size())
+ }
+ return nil
+}
+
+func (c *Stanza) newStanza(name string) *Stanza {
+ c.append("\n" + name + " {")
+ c.pad += 2
+ return c
+}
+
+func (c *Stanza) append(name string) *Stanza {
+ c.content += strings.Repeat(" ", c.pad)
+ c.content += name + "\n"
+ return c
+}
+
+func (c *Stanza) close() *Stanza {
+ c.content += "}\n"
+ c.pad -= 2
+ return c
+}
+
+func (s *Stanza) toString() string {
+ return s.content
+}
+
+func (s *Stanza) saveToFile(fileName string) error {
+ fo, err := os.Create(fileName)
+ if err != nil {
+ return err
+ }
+ defer fo.Close()
+
+ _, err = io.Copy(fo, strings.NewReader(s.content))
+ return err
+}
+
+// newHttpClient creates [http.Client] with disabled proxy and redirects, it also sets timeout to 30seconds.
+func newHttpClient() *http.Client {
+ transport := http.DefaultTransport
+ transport.(*http.Transport).Proxy = nil
+ transport.(*http.Transport).DisableKeepAlives = true
+ client := &http.Client{
+ Transport: transport,
+ Timeout: time.Second * 30,
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ }}
+ return client
+}
diff --git a/extras/hs-test/vars b/extras/hs-test/vars
new file mode 100644
index 00000000000..d1ca078fe21
--- /dev/null
+++ b/extras/hs-test/vars
@@ -0,0 +1,7 @@
+export VPP_WS=../../
+
+export HST_LDPRELOAD=${VPP_WS}/build-root/build-vpp-native/vpp/lib/x86_64-linux-gnu/libvcl_ldpreload.so
+export PATH=${VPP_WS}/build-root/build-vpp-native/vpp/bin:$PATH
+
+export UBUNTU_VERSION=$(lsb_release -rs)
+export HST_EXTENDED_TESTS=false
diff --git a/extras/hs-test/vcl_test.go b/extras/hs-test/vcl_test.go
new file mode 100644
index 00000000000..fdcd60ad503
--- /dev/null
+++ b/extras/hs-test/vcl_test.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func init() {
+ registerVethTests(XEchoVclClientUdpTest, XEchoVclClientTcpTest, XEchoVclServerUdpTest,
+ XEchoVclServerTcpTest, VclEchoTcpTest, VclEchoUdpTest, VclRetryAttachTest)
+}
+
+func getVclConfig(c *Container, ns_id_optional ...string) string {
+ var s Stanza
+ ns_id := "default"
+ if len(ns_id_optional) > 0 {
+ ns_id = ns_id_optional[0]
+ }
+ s.newStanza("vcl").
+ append(fmt.Sprintf("app-socket-api %[1]s/var/run/app_ns_sockets/%[2]s", c.getContainerWorkDir(), ns_id)).
+ append("app-scope-global").
+ append("app-scope-local").
+ append("use-mq-eventfd")
+ if len(ns_id_optional) > 0 {
+ s.append(fmt.Sprintf("namespace-id %[1]s", ns_id)).
+ append(fmt.Sprintf("namespace-secret %[1]s", ns_id))
+ }
+ return s.close().toString()
+}
+
+func XEchoVclClientUdpTest(s *VethsSuite) {
+ s.testXEchoVclClient("udp")
+}
+
+func XEchoVclClientTcpTest(s *VethsSuite) {
+ s.testXEchoVclClient("tcp")
+}
+
+func (s *VethsSuite) testXEchoVclClient(proto string) {
+ port := "12345"
+ serverVpp := s.getContainerByName("server-vpp").vppInstance
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ serverVpp.vppctl("test echo server uri %s://%s/%s fifo-size 64k", proto, serverVeth.ip4AddressString(), port)
+
+ echoClnContainer := s.getTransientContainerByName("client-app")
+ echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer))
+
+ testClientCommand := "vcl_test_client -N 100 -p " + proto + " " + serverVeth.ip4AddressString() + " " + port
+ s.log(testClientCommand)
+ echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ o := echoClnContainer.exec(testClientCommand)
+ s.log(o)
+ s.assertContains(o, "CLIENT RESULTS")
+}
+
+func XEchoVclServerUdpTest(s *VethsSuite) {
+ s.testXEchoVclServer("udp")
+}
+
+func XEchoVclServerTcpTest(s *VethsSuite) {
+ s.testXEchoVclServer("tcp")
+}
+
+func (s *VethsSuite) testXEchoVclServer(proto string) {
+ port := "12345"
+ srvVppCont := s.getContainerByName("server-vpp")
+ srvAppCont := s.getContainerByName("server-app")
+
+ srvAppCont.createFile("/vcl.conf", getVclConfig(srvVppCont))
+ srvAppCont.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ vclSrvCmd := fmt.Sprintf("vcl_test_server -p %s %s", proto, port)
+ srvAppCont.execServer(vclSrvCmd)
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ serverVethAddress := serverVeth.ip4AddressString()
+
+ clientVpp := s.getContainerByName("client-vpp").vppInstance
+ o := clientVpp.vppctl("test echo client uri %s://%s/%s fifo-size 64k verbose mbytes 2", proto, serverVethAddress, port)
+ s.log(o)
+ s.assertContains(o, "Test finished at")
+}
+
+func (s *VethsSuite) testVclEcho(proto string) {
+ port := "12345"
+ srvVppCont := s.getContainerByName("server-vpp")
+ srvAppCont := s.getContainerByName("server-app")
+
+ srvAppCont.createFile("/vcl.conf", getVclConfig(srvVppCont))
+ srvAppCont.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ srvAppCont.execServer("vcl_test_server " + port)
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ serverVethAddress := serverVeth.ip4AddressString()
+
+ echoClnContainer := s.getTransientContainerByName("client-app")
+ echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer))
+
+ testClientCommand := "vcl_test_client -p " + proto + " " + serverVethAddress + " " + port
+ echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ o := echoClnContainer.exec(testClientCommand)
+ s.log(o)
+}
+
+func VclEchoTcpTest(s *VethsSuite) {
+ s.testVclEcho("tcp")
+}
+
+func VclEchoUdpTest(s *VethsSuite) {
+ s.testVclEcho("udp")
+}
+
+func VclRetryAttachTest(s *VethsSuite) {
+ s.testRetryAttach("tcp")
+}
+
+func (s *VethsSuite) testRetryAttach(proto string) {
+ srvVppContainer := s.getTransientContainerByName("server-vpp")
+
+ echoSrvContainer := s.getContainerByName("server-app")
+
+ echoSrvContainer.createFile("/vcl.conf", getVclConfig(echoSrvContainer))
+
+ echoSrvContainer.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ echoSrvContainer.execServer("vcl_test_server -p " + proto + " 12346")
+
+ s.log("This whole test case can take around 3 minutes to run. Please be patient.")
+ s.log("... Running first echo client test, before disconnect.")
+
+ serverVeth := s.getInterfaceByName(serverInterfaceName)
+ serverVethAddress := serverVeth.ip4AddressString()
+
+ echoClnContainer := s.getTransientContainerByName("client-app")
+ echoClnContainer.createFile("/vcl.conf", getVclConfig(echoClnContainer))
+
+ testClientCommand := "vcl_test_client -U -p " + proto + " " + serverVethAddress + " 12346"
+ echoClnContainer.addEnvVar("VCL_CONFIG", "/vcl.conf")
+ o := echoClnContainer.exec(testClientCommand)
+ s.log(o)
+ s.log("... First test ended. Stopping VPP server now.")
+
+ // Stop server-vpp-instance, start it again and then run vcl-test-client once more
+ srvVppContainer.vppInstance.disconnect()
+ stopVppCommand := "/bin/bash -c 'ps -C vpp_main -o pid= | xargs kill -9'"
+ srvVppContainer.exec(stopVppCommand)
+
+ s.setupServerVpp()
+
+ s.log("... VPP server is starting again, so waiting for a bit.")
+ time.Sleep(30 * time.Second) // Wait a moment for the re-attachment to happen
+
+ s.log("... Running second echo client test, after disconnect and re-attachment.")
+ o = echoClnContainer.exec(testClientCommand)
+ s.log(o)
+ s.log("Done.")
+}
diff --git a/extras/hs-test/vppinstance.go b/extras/hs-test/vppinstance.go
new file mode 100644
index 00000000000..8a92776894c
--- /dev/null
+++ b/extras/hs-test/vppinstance.go
@@ -0,0 +1,476 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "os/signal"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/edwarnicke/exechelper"
+ . "github.com/onsi/ginkgo/v2"
+ "github.com/sirupsen/logrus"
+
+ "go.fd.io/govpp"
+ "go.fd.io/govpp/api"
+ "go.fd.io/govpp/binapi/af_packet"
+ interfaces "go.fd.io/govpp/binapi/interface"
+ "go.fd.io/govpp/binapi/interface_types"
+ "go.fd.io/govpp/binapi/session"
+ "go.fd.io/govpp/binapi/tapv2"
+ "go.fd.io/govpp/core"
+)
+
+const vppConfigTemplate = `unix {
+ nodaemon
+ log %[1]s%[4]s
+ full-coredump
+ cli-listen %[1]s%[2]s
+ runtime-dir %[1]s/var/run
+ gid vpp
+}
+
+api-trace {
+ on
+}
+
+api-segment {
+ gid vpp
+}
+
+socksvr {
+ socket-name %[1]s%[3]s
+}
+
+statseg {
+ socket-name %[1]s/var/run/vpp/stats.sock
+}
+
+plugins {
+ plugin default { disable }
+
+ plugin unittest_plugin.so { enable }
+ plugin quic_plugin.so { enable }
+ plugin af_packet_plugin.so { enable }
+ plugin hs_apps_plugin.so { enable }
+ plugin http_plugin.so { enable }
+ plugin http_static_plugin.so { enable }
+ plugin prom_plugin.so { enable }
+ plugin tlsopenssl_plugin.so { enable }
+ plugin ping_plugin.so { enable }
+ plugin nsim_plugin.so { enable }
+}
+
+logging {
+ default-log-level debug
+ default-syslog-log-level debug
+}
+
+`
+
+const (
+ defaultCliSocketFilePath = "/var/run/vpp/cli.sock"
+ defaultApiSocketFilePath = "/var/run/vpp/api.sock"
+ defaultLogFilePath = "/var/log/vpp/vpp.log"
+)
+
+type VppInstance struct {
+ container *Container
+ additionalConfig []Stanza
+ connection *core.Connection
+ apiStream api.Stream
+ cpus []int
+}
+
+func (vpp *VppInstance) getSuite() *HstSuite {
+ return vpp.container.suite
+}
+
+func (vpp *VppInstance) getCliSocket() string {
+ return fmt.Sprintf("%s%s", vpp.container.getContainerWorkDir(), defaultCliSocketFilePath)
+}
+
+func (vpp *VppInstance) getRunDir() string {
+ return vpp.container.getContainerWorkDir() + "/var/run/vpp"
+}
+
+func (vpp *VppInstance) getLogDir() string {
+ return vpp.container.getContainerWorkDir() + "/var/log/vpp"
+}
+
+func (vpp *VppInstance) getEtcDir() string {
+ return vpp.container.getContainerWorkDir() + "/etc/vpp"
+}
+
+func (vpp *VppInstance) start() error {
+ maxReconnectAttempts := 3
+ // Replace default logger in govpp with our own
+ govppLogger := logrus.New()
+ govppLogger.SetOutput(io.MultiWriter(vpp.getSuite().logger.Writer(), GinkgoWriter))
+ core.SetLogger(govppLogger)
+ // Create folders
+ containerWorkDir := vpp.container.getContainerWorkDir()
+
+ vpp.container.exec("mkdir --mode=0700 -p " + vpp.getRunDir())
+ vpp.container.exec("mkdir --mode=0700 -p " + vpp.getLogDir())
+ vpp.container.exec("mkdir --mode=0700 -p " + vpp.getEtcDir())
+
+ // Create startup.conf inside the container
+ configContent := fmt.Sprintf(
+ vppConfigTemplate,
+ containerWorkDir,
+ defaultCliSocketFilePath,
+ defaultApiSocketFilePath,
+ defaultLogFilePath,
+ )
+ configContent += vpp.generateCpuConfig()
+ for _, c := range vpp.additionalConfig {
+ configContent += c.toString()
+ }
+ startupFileName := vpp.getEtcDir() + "/startup.conf"
+ vpp.container.createFile(startupFileName, configContent)
+
+ // create wrapper script for vppctl with proper CLI socket path
+ cliContent := "#!/usr/bin/bash\nvppctl -s " + vpp.getRunDir() + "/cli.sock"
+ vppcliFileName := "/usr/bin/vppcli"
+ vpp.container.createFile(vppcliFileName, cliContent)
+ vpp.container.exec("chmod 0755 " + vppcliFileName)
+
+ vpp.getSuite().log("starting vpp")
+ if *isVppDebug {
+ // default = 3; VPP will timeout while debugging if there are not enough attempts
+ maxReconnectAttempts = 5000
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, syscall.SIGQUIT)
+ cont := make(chan bool, 1)
+ go func() {
+ <-sig
+ cont <- true
+ }()
+
+ vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
+ fmt.Println("run following command in different terminal:")
+ fmt.Println("docker exec -it " + vpp.container.name + " gdb -ex \"attach $(docker exec " + vpp.container.name + " pidof vpp)\"")
+ fmt.Println("Afterwards press CTRL+\\ to continue")
+ <-cont
+ fmt.Println("continuing...")
+ } else {
+ // Start VPP
+ vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
+ }
+
+ vpp.getSuite().log("connecting to vpp")
+ // Connect to VPP and store the connection
+ sockAddress := vpp.container.getHostWorkDir() + defaultApiSocketFilePath
+ conn, connEv, err := govpp.AsyncConnect(
+ sockAddress,
+ maxReconnectAttempts,
+ core.DefaultReconnectInterval)
+ if err != nil {
+ vpp.getSuite().log("async connect error: " + fmt.Sprint(err))
+ return err
+ }
+ vpp.connection = conn
+
+ // ... wait for Connected event
+ e := <-connEv
+ if e.State != core.Connected {
+ vpp.getSuite().log("connecting to VPP failed: " + fmt.Sprint(e.Error))
+ }
+
+ ch, err := conn.NewStream(
+ context.Background(),
+ core.WithRequestSize(50),
+ core.WithReplySize(50),
+ core.WithReplyTimeout(time.Second*5))
+ if err != nil {
+ vpp.getSuite().log("creating stream failed: " + fmt.Sprint(err))
+ return err
+ }
+ vpp.apiStream = ch
+
+ return nil
+}
+
+func (vpp *VppInstance) vppctl(command string, arguments ...any) string {
+ vppCliCommand := fmt.Sprintf(command, arguments...)
+ containerExecCommand := fmt.Sprintf("docker exec --detach=false %[1]s vppctl -s %[2]s %[3]s",
+ vpp.container.name, vpp.getCliSocket(), vppCliCommand)
+ vpp.getSuite().log(containerExecCommand)
+ output, err := exechelper.CombinedOutput(containerExecCommand)
+ vpp.getSuite().assertNil(err)
+
+ return string(output)
+}
+
+func (vpp *VppInstance) GetSessionStat(stat string) int {
+ o := vpp.vppctl("show session stats")
+ vpp.getSuite().log(o)
+ for _, line := range strings.Split(o, "\n") {
+ if strings.Contains(line, stat) {
+ tokens := strings.Split(strings.TrimSpace(line), " ")
+ val, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ Fail("failed to parse stat value %s" + fmt.Sprint(err))
+ return 0
+ }
+ return val
+ }
+ }
+ return 0
+}
+
+func (vpp *VppInstance) waitForApp(appName string, timeout int) {
+ vpp.getSuite().log("waiting for app " + appName)
+ for i := 0; i < timeout; i++ {
+ o := vpp.vppctl("show app")
+ if strings.Contains(o, appName) {
+ return
+ }
+ time.Sleep(1 * time.Second)
+ }
+ vpp.getSuite().assertNil(1, "Timeout while waiting for app '%s'", appName)
+}
+
+func (vpp *VppInstance) createAfPacket(
+ veth *NetInterface,
+) (interface_types.InterfaceIndex, error) {
+ createReq := &af_packet.AfPacketCreateV3{
+ Mode: 1,
+ UseRandomHwAddr: true,
+ HostIfName: veth.Name(),
+ Flags: af_packet.AfPacketFlags(11),
+ }
+ if veth.hwAddress != (MacAddress{}) {
+ createReq.UseRandomHwAddr = false
+ createReq.HwAddr = veth.hwAddress
+ }
+
+ vpp.getSuite().log("create af-packet interface " + veth.Name())
+ if err := vpp.apiStream.SendMsg(createReq); err != nil {
+ vpp.getSuite().hstFail()
+ return 0, err
+ }
+ replymsg, err := vpp.apiStream.RecvMsg()
+ if err != nil {
+ return 0, err
+ }
+ reply := replymsg.(*af_packet.AfPacketCreateV3Reply)
+ err = api.RetvalToVPPApiError(reply.Retval)
+ if err != nil {
+ return 0, err
+ }
+
+ veth.index = reply.SwIfIndex
+
+ // Set to up
+ upReq := &interfaces.SwInterfaceSetFlags{
+ SwIfIndex: veth.index,
+ Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
+ }
+
+ vpp.getSuite().log("set af-packet interface " + veth.Name() + " up")
+ if err := vpp.apiStream.SendMsg(upReq); err != nil {
+ return 0, err
+ }
+ replymsg, err = vpp.apiStream.RecvMsg()
+ if err != nil {
+ return 0, err
+ }
+ reply2 := replymsg.(*interfaces.SwInterfaceSetFlagsReply)
+ if err = api.RetvalToVPPApiError(reply2.Retval); err != nil {
+ return 0, err
+ }
+
+ // Add address
+ if veth.addressWithPrefix() == (AddressWithPrefix{}) {
+ var err error
+ var ip4Address string
+ if ip4Address, err = veth.ip4AddrAllocator.NewIp4InterfaceAddress(veth.peer.networkNumber); err == nil {
+ veth.ip4Address = ip4Address
+ } else {
+ return 0, err
+ }
+ }
+ addressReq := &interfaces.SwInterfaceAddDelAddress{
+ IsAdd: true,
+ SwIfIndex: veth.index,
+ Prefix: veth.addressWithPrefix(),
+ }
+
+ vpp.getSuite().log("af-packet interface " + veth.Name() + " add address " + veth.ip4Address)
+ if err := vpp.apiStream.SendMsg(addressReq); err != nil {
+ return 0, err
+ }
+ replymsg, err = vpp.apiStream.RecvMsg()
+ if err != nil {
+ return 0, err
+ }
+ reply3 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply)
+ err = api.RetvalToVPPApiError(reply3.Retval)
+ if err != nil {
+ return 0, err
+ }
+
+ return veth.index, nil
+}
+
+func (vpp *VppInstance) addAppNamespace(
+ secret uint64,
+ ifx interface_types.InterfaceIndex,
+ namespaceId string,
+) error {
+ req := &session.AppNamespaceAddDelV4{
+ IsAdd: true,
+ Secret: secret,
+ SwIfIndex: ifx,
+ NamespaceID: namespaceId,
+ SockName: defaultApiSocketFilePath,
+ }
+
+ vpp.getSuite().log("add app namespace " + namespaceId)
+ if err := vpp.apiStream.SendMsg(req); err != nil {
+ return err
+ }
+ replymsg, err := vpp.apiStream.RecvMsg()
+ if err != nil {
+ return err
+ }
+ reply := replymsg.(*session.AppNamespaceAddDelV4Reply)
+ if err = api.RetvalToVPPApiError(reply.Retval); err != nil {
+ return err
+ }
+
+ sessionReq := &session.SessionEnableDisable{
+ IsEnable: true,
+ }
+
+ vpp.getSuite().log("enable app namespace " + namespaceId)
+ if err := vpp.apiStream.SendMsg(sessionReq); err != nil {
+ return err
+ }
+ replymsg, err = vpp.apiStream.RecvMsg()
+ if err != nil {
+ return err
+ }
+ reply2 := replymsg.(*session.SessionEnableDisableReply)
+ if err = api.RetvalToVPPApiError(reply2.Retval); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (vpp *VppInstance) createTap(
+ tap *NetInterface,
+ tapId ...uint32,
+) error {
+ var id uint32 = 1
+ if len(tapId) > 0 {
+ id = tapId[0]
+ }
+ createTapReq := &tapv2.TapCreateV3{
+ ID: id,
+ HostIfNameSet: true,
+ HostIfName: tap.Name(),
+ HostIP4PrefixSet: true,
+ HostIP4Prefix: tap.ip4AddressWithPrefix(),
+ }
+
+ vpp.getSuite().log("create tap interface " + tap.Name())
+ // Create tap interface
+ if err := vpp.apiStream.SendMsg(createTapReq); err != nil {
+ return err
+ }
+ replymsg, err := vpp.apiStream.RecvMsg()
+ if err != nil {
+ return err
+ }
+ reply := replymsg.(*tapv2.TapCreateV3Reply)
+ if err = api.RetvalToVPPApiError(reply.Retval); err != nil {
+ return err
+ }
+
+ // Add address
+ addAddressReq := &interfaces.SwInterfaceAddDelAddress{
+ IsAdd: true,
+ SwIfIndex: reply.SwIfIndex,
+ Prefix: tap.peer.addressWithPrefix(),
+ }
+
+ vpp.getSuite().log("tap interface " + tap.Name() + " add address " + tap.peer.ip4Address)
+ if err := vpp.apiStream.SendMsg(addAddressReq); err != nil {
+ return err
+ }
+ replymsg, err = vpp.apiStream.RecvMsg()
+ if err != nil {
+ return err
+ }
+ reply2 := replymsg.(*interfaces.SwInterfaceAddDelAddressReply)
+ if err = api.RetvalToVPPApiError(reply2.Retval); err != nil {
+ return err
+ }
+
+ // Set interface to up
+ upReq := &interfaces.SwInterfaceSetFlags{
+ SwIfIndex: reply.SwIfIndex,
+ Flags: interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
+ }
+
+ vpp.getSuite().log("set tap interface " + tap.Name() + " up")
+ if err := vpp.apiStream.SendMsg(upReq); err != nil {
+ return err
+ }
+ replymsg, err = vpp.apiStream.RecvMsg()
+ if err != nil {
+ return err
+ }
+ reply3 := replymsg.(*interfaces.SwInterfaceSetFlagsReply)
+ if err = api.RetvalToVPPApiError(reply3.Retval); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (vpp *VppInstance) saveLogs() {
+ logTarget := vpp.container.getLogDirPath() + "vppinstance-" + vpp.container.name + ".log"
+ logSource := vpp.container.getHostWorkDir() + defaultLogFilePath
+ cmd := exec.Command("cp", logSource, logTarget)
+ vpp.getSuite().log(cmd.String())
+ cmd.Run()
+}
+
+func (vpp *VppInstance) disconnect() {
+ vpp.connection.Disconnect()
+ vpp.apiStream.Close()
+}
+
+func (vpp *VppInstance) generateCpuConfig() string {
+ var c Stanza
+ var s string
+ if len(vpp.cpus) < 1 {
+ return ""
+ }
+ c.newStanza("cpu").
+ append(fmt.Sprintf("main-core %d", vpp.cpus[0]))
+ vpp.getSuite().log(fmt.Sprintf("main-core %d", vpp.cpus[0]))
+ workers := vpp.cpus[1:]
+
+ if len(workers) > 0 {
+ for i := 0; i < len(workers); i++ {
+ if i != 0 {
+ s = s + ", "
+ }
+ s = s + fmt.Sprintf("%d", workers[i])
+ }
+ c.append(fmt.Sprintf("corelist-workers %s", s))
+ vpp.getSuite().log("corelist-workers " + s)
+ }
+ return c.close().toString()
+}