From 2efe1335d93d6697b946c3189de688adfbb682b9 Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Mon, 28 Sep 2026 13:45:21 +0200 Subject: [PATCH 1/2] test: containers: netconf port forwarding Add a test for NETCONF agents running in containers. It checks that port 830 on an external interface is forwarded to the container, and that the container can connect to NETCONF on port 830 of the host over the internal network. Resolves: #1666 Signed-off-by: Ejub Sabic --- test/case/containers/Readme.adoc | 5 + test/case/containers/all.yaml | 3 + .../netconf_port_forward/Readme.adoc | 1 + .../containers/netconf_port_forward/test.adoc | 51 +++++ .../containers/netconf_port_forward/test.py | 213 ++++++++++++++++++ .../netconf_port_forward/topology.dot | 24 ++ .../netconf_port_forward/topology.svg | 45 ++++ test/infamy/netutil.py | 8 + 8 files changed, 350 insertions(+) create mode 120000 test/case/containers/netconf_port_forward/Readme.adoc create mode 100644 test/case/containers/netconf_port_forward/test.adoc create mode 100755 test/case/containers/netconf_port_forward/test.py create mode 100644 test/case/containers/netconf_port_forward/topology.dot create mode 100644 test/case/containers/netconf_port_forward/topology.svg diff --git a/test/case/containers/Readme.adoc b/test/case/containers/Readme.adoc index a0e1fb32a..d11326352 100644 --- a/test/case/containers/Readme.adoc +++ b/test/case/containers/Readme.adoc @@ -12,6 +12,7 @@ Tests verifying link:https://opencontainers.org/[OCI container] support: - Container upgrades with persistent volume data - Container upgrade using RPC with cleanup of old image - Firewall container running in host network mode with full privileges + - NETCONF port 830 forwarded to a container, which connects back to NETCONF on the host include::basic/Readme.adoc[] @@ -50,3 +51,7 @@ include::firewall_basic/Readme.adoc[] <<< include::host_commands/Readme.adoc[] + +<<< + +include::netconf_port_forward/Readme.adoc[] diff --git a/test/case/containers/all.yaml b/test/case/containers/all.yaml index ea4ab2d8f..8b391acd9 100644 --- a/test/case/containers/all.yaml +++ b/test/case/containers/all.yaml @@ -32,3 +32,6 @@ - name: Host Command Execution from Container case: host_commands/test.py + +- name: Container NETCONF Port Forwarding + case: netconf_port_forward/test.py diff --git a/test/case/containers/netconf_port_forward/Readme.adoc b/test/case/containers/netconf_port_forward/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/containers/netconf_port_forward/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/containers/netconf_port_forward/test.adoc b/test/case/containers/netconf_port_forward/test.adoc new file mode 100644 index 000000000..4a434c65d --- /dev/null +++ b/test/case/containers/netconf_port_forward/test.adoc @@ -0,0 +1,51 @@ +=== Container NETCONF Port Forwarding + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/netconf_port_forward] + +==== Description + +Verify that a NETCONF agent in a container can be reached on port 830 +from the outside, and that the agent can reach the NETCONF server on +the target over the internal network. + +.... + <-- container --> +.-------------. .----------------------. .---------------. +| | mgmt |------------| mgmt | | | | | nc | +| host | data |------------| ext0 | target | int0 |------| eth0 | agent | +'-------------'.42 .1'----------------------'.1 .2'---------------' + 192.168.0.0/24 10.0.0.0/24 + VETH pair +.... + +The target's firewall puts `ext0` in a `wan` zone, which drops all +traffic to the target except TCP port 830. That port is forwarded to +the container at 10.0.0.2:830, so a connection to port 830 on `ext0` +ends up in the container, not at the target's NETCONF server. The +target end of the VETH pair, `int0`, is in an `int` zone that allows +only the `netconf` service. + +The agent is a netcat listener on port 830. For each connection it +prints a known greeting, connects to the target at 10.0.0.1:830, and +relays the first line it receives, which is the SSH banner of the +target's NETCONF server. + +The test host connects to 192.168.0.1:830. If the greeting arrives, +the port forward works. If the SSH banner follows it, the container +reached NETCONF on the target. + +==== Topology + +image::topology.svg[Container NETCONF Port Forwarding topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure ext0 and VETH pair for agent container +. Forward port 830 on ext0 to agent container +. Create agent container from bundled OCI image +. Verify agent container has started +. Verify port 830 on ext0 reaches the agent container +. Verify agent container reaches NETCONF on the target + + diff --git a/test/case/containers/netconf_port_forward/test.py b/test/case/containers/netconf_port_forward/test.py new file mode 100755 index 000000000..85615fc04 --- /dev/null +++ b/test/case/containers/netconf_port_forward/test.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +r"""Container NETCONF Port Forwarding + +Verify that a NETCONF agent in a container can be reached on port 830 +from the outside, and that the agent can reach the NETCONF server on +the target over the internal network. + +.... + <-- container --> +.-------------. .----------------------. .---------------. +| | mgmt |------------| mgmt | | | | | nc | +| host | data |------------| ext0 | target | int0 |------| eth0 | agent | +'-------------'.42 .1'----------------------'.1 .2'---------------' + 192.168.0.0/24 10.0.0.0/24 + VETH pair +.... + +The target's firewall puts `ext0` in a `wan` zone, which drops all +traffic to the target except TCP port 830. That port is forwarded to +the container at 10.0.0.2:830, so a connection to port 830 on `ext0` +ends up in the container, not at the target's NETCONF server. The +target end of the VETH pair, `int0`, is in an `int` zone that allows +only the `netconf` service. + +The agent is a netcat listener on port 830. For each connection it +prints a known greeting, connects to the target at 10.0.0.1:830, and +relays the first line it receives, which is the SSH banner of the +target's NETCONF server. + +The test host connects to 192.168.0.1:830. If the greeting arrives, +the port forward works. If the SSH banner follows it, the container +reached NETCONF on the target. +""" +import infamy +from infamy import netutil +from infamy.util import until, to_binary + + +with infamy.Test() as test: + NFTABLES = f"oci-archive:{infamy.Container.NFTABLES_IMAGE}" + NETCONF_CONTAINER_IP = "10.0.0.2" + INTIP = "10.0.0.1" + EXTIP = "192.168.0.1" + OURIP = "192.168.0.42" + NETCONF_CONTAINER = "netconf_container" + NETCONF_IF = "netconf0" + GREETING = "Hello from the NETCONF agent container" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, mgmt = env.ltop.xlate("target", "mgmt") + _, ext0 = env.ltop.xlate("target", "ext0") + _, hport = env.ltop.xlate("host", "data") + + if not target.has_model("infix-containers"): + test.skip() + if not target.has_model("infix-firewall"): + test.skip() + + with test.step("Configure ext0 and VETH pair for agent container"): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": ext0, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": EXTIP, + "prefix-length": 24 + }] + } + }, + { + "name": "int0", + "type": "infix-if-type:veth", + "enabled": True, + "infix-interfaces:veth": { + "peer": NETCONF_IF + }, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": INTIP, + "prefix-length": 24 + }] + } + }, + { + "name": NETCONF_IF, + "type": "infix-if-type:veth", + "enabled": True, + "infix-interfaces:veth": { + "peer": "int0" + }, + "ipv4": { + "address": [{ + "ip": NETCONF_CONTAINER_IP, + "prefix-length": 24 + }] + }, + "container-network": { + "route": [{ + "subnet": "0.0.0.0/0", + "gateway": INTIP + }] + } + } + ] + } + } + }) + + with test.step("Forward port 830 on ext0 to agent container"): + target.put_config_dicts({ + "infix-firewall": { + "firewall": { + "default": "wan", + "zone": [ + { + "name": "wan", + "action": "drop", + "interface": [ext0], + "port-forward": [{ + "lower": 830, + "proto": "tcp", + "to": { + "addr": NETCONF_CONTAINER_IP + } + }] + }, + { + "name": "int", + "action": "reject", + "interface": ["int0"], + "service": ["netconf"] + }, + { + "name": "mgmt", + "action": "accept", + "interface": [mgmt] + } + ] + } + } + }) + + infamy.Firewall.wait_for_operational(target, { + "wan": {"action": "drop"}, + "int": {"action": "reject"}, + "mgmt": {"action": "accept"} + }) + + with test.step("Create agent container from bundled OCI image"): + agent = to_binary(f"""#!/bin/sh +echo "{GREETING}" +# nc exits when stdin closes, before the server has sent its banner +sleep 3 | timeout 5 nc {INTIP} 830 | head -n 1 +""") + rclocal = to_binary("""#!/bin/sh +nc -lk -p 830 -e /usr/bin/agent & +""") + + target.put_config_dicts({ + "infix-containers": { + "containers": { + "container": [ + { + "name": NETCONF_CONTAINER, + "image": NFTABLES, + "network": { + "interface": [ + {"name": NETCONF_IF} + ] + }, + "mount": [ + { + "name": "agent", + "content": agent, + "target": "/usr/bin/agent", + "mode": "0755" + }, + { + "name": "rc.local", + "content": rclocal, + "target": "/etc/rc.local", + "mode": "0755" + } + ] + } + ] + } + } + }) + + with test.step("Verify agent container has started"): + c = infamy.Container(target) + until(lambda: c.running(NETCONF_CONTAINER), attempts=60) + + with infamy.IsolatedMacVlan(hport) as ns: + ns.addip(OURIP) + + with test.step("Verify port 830 on ext0 reaches the agent container"): + until(lambda: GREETING in ns.call( + lambda: netutil.tcp_read(EXTIP, 830)), attempts=30) + + with test.step("Verify agent container reaches NETCONF on the target"): + until(lambda: "SSH-2.0-" in ns.call( + lambda: netutil.tcp_read(EXTIP, 830)), attempts=10) + + test.succeed() diff --git a/test/case/containers/netconf_port_forward/topology.dot b/test/case/containers/netconf_port_forward/topology.dot new file mode 100644 index 000000000..cb15f9d71 --- /dev/null +++ b/test/case/containers/netconf_port_forward/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | ext0 } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:ext0 [color=black, headlabel=".1 ", taillabel=" .42", label="\n 192.168.0.0/24 "] +} diff --git a/test/case/containers/netconf_port_forward/topology.svg b/test/case/containers/netconf_port_forward/topology.svg new file mode 100644 index 000000000..7ad4cdb72 --- /dev/null +++ b/test/case/containers/netconf_port_forward/topology.svg @@ -0,0 +1,45 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +ext0 + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:ext0 + +     192.168.0.0/24      +.1 + .42 + + + diff --git a/test/infamy/netutil.py b/test/infamy/netutil.py index cdc9a5520..847a75432 100644 --- a/test/infamy/netutil.py +++ b/test/infamy/netutil.py @@ -6,3 +6,11 @@ def tcp_port_is_open(host, port, timeout=3): return True except (socket.timeout, OSError): return False + +def tcp_read(host, port, timeout=10): + """Read everything the server sends until it closes the connection""" + data = b"" + with socket.create_connection((host, port), timeout=timeout) as sock: + while chunk := sock.recv(1024): + data += chunk + return data.decode(errors="replace") From 34e986a2ef45740ee5135ac103076258fb705932 Mon Sep 17 00:00:00 2001 From: Ejub Sabic Date: Mon, 28 Sep 2026 13:46:30 +0200 Subject: [PATCH 2/2] test: add missing artifacts from make-test spec on main Signed-off-by: Ejub Sabic --- test/case/misc/support_collect/test.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/case/misc/support_collect/test.adoc b/test/case/misc/support_collect/test.adoc index 14e68ce05..037047d8c 100644 --- a/test/case/misc/support_collect/test.adoc +++ b/test/case/misc/support_collect/test.adoc @@ -1,6 +1,6 @@ === Support Data Collection -ifdef::topdoc[:imagesdir: {topdoc}../../misc/support_collect] +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/misc/support_collect] ==== Description