Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions test/case/containers/Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]

Expand Down Expand Up @@ -50,3 +51,7 @@ include::firewall_basic/Readme.adoc[]
<<<

include::host_commands/Readme.adoc[]

<<<

include::netconf_port_forward/Readme.adoc[]
3 changes: 3 additions & 0 deletions test/case/containers/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions test/case/containers/netconf_port_forward/Readme.adoc
51 changes: 51 additions & 0 deletions test/case/containers/netconf_port_forward/test.adoc
Original file line number Diff line number Diff line change
@@ -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


213 changes: 213 additions & 0 deletions test/case/containers/netconf_port_forward/test.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 24 additions & 0 deletions test/case/containers/netconf_port_forward/topology.dot
Original file line number Diff line number Diff line change
@@ -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> mgmt | <data> data }",
pos="0,12!",
requires="controller",
];

target [
label="{ <mgmt> mgmt | <ext0> 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 "]
}
45 changes: 45 additions & 0 deletions test/case/containers/netconf_port_forward/topology.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test/case/misc/support_collect/test.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
=== Support Data Collection

ifdef::topdoc[:imagesdir: {topdoc}../../misc/support_collect]
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/misc/support_collect]

==== Description

Expand Down
8 changes: 8 additions & 0 deletions test/infamy/netutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading