diff --git a/test/case/infix_containers/Readme.adoc b/test/case/infix_containers/Readme.adoc index 4eb951db7..e67eb1b03 100644 --- a/test/case/infix_containers/Readme.adoc +++ b/test/case/infix_containers/Readme.adoc @@ -1,6 +1,14 @@ :testgroup: == infix-containers +Verifies Infix Docker container support: + + - Basic web server container running in host network mode + - Common setup with a docker0 bridge, automatic VETH pairs to container(s) + - Connecting a container with a VETH pair to a standard Linux bridge + - Assigning a physical Ethernet interface to a container + - Basic firewall container running in host network mode and full privileges + <<< include::container_basic/Readme.adoc[] @@ -10,3 +18,5 @@ include::container_bridge/Readme.adoc[] include::container_phys/Readme.adoc[] include::container_veth/Readme.adoc[] + +include::container_firewall_basic/Readme.adoc[] diff --git a/test/case/infix_containers/container_basic/test.py b/test/case/infix_containers/container_basic/test.py index d95059d7d..4415a902c 100755 --- a/test/case/infix_containers/container_basic/test.py +++ b/test/case/infix_containers/container_basic/test.py @@ -20,7 +20,7 @@ def _verify(server): - # Should really use mDNS here.... + # TODO: Should really use mDNS here.... url = infamy.Furl(f"http://[{server}]:91/index.html") return url.check("It works") @@ -49,7 +49,7 @@ def _verify(server): "container": [ { "name": f"{NAME}", - "image": f"oci-archive:{infamy.Container.IMAGE}", + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", "command": "/usr/sbin/httpd -f -v -p 91", "network": { "host": True diff --git a/test/case/infix_containers/container_bridge/test.py b/test/case/infix_containers/container_bridge/test.py index ea4ebd378..627ca874a 100755 --- a/test/case/infix_containers/container_bridge/test.py +++ b/test/case/infix_containers/container_bridge/test.py @@ -16,11 +16,10 @@ """ import base64 import infamy -from infamy.util import until +from infamy.util import until, to_binary with infamy.Test() as test: NAME = "web-docker0" - IMAGE = "curios-httpd-edge.tar.gz" DUTIP = "10.0.0.2" OURIP = "10.0.0.1" BODY = "
Kilroy was here
" @@ -35,7 +34,7 @@ with test.step("Create container 'web-docker0' from bundled OCI image"): _, ifname = env.ltop.xlate("target", "data") - enc = base64.b64encode(BODY.encode('utf-8')) + data = to_binary(BODY) target.put_config_dict("ietf-interfaces", { "interfaces": { "interface": [ @@ -67,12 +66,12 @@ "container": [ { "name": f"{NAME}", - "image": f"oci-archive:{infamy.Container.IMAGE}", + "image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}", "command": "/usr/sbin/httpd -f -v -p 91", "mount": [ { "name": "index.html", - "content": f"{enc.decode('utf-8')}", + "content": f"{data}", "target": "/var/www/index.html" } ], diff --git a/test/case/infix_containers/container_firewall_basic/Readme.adoc b/test/case/infix_containers/container_firewall_basic/Readme.adoc new file mode 100644 index 000000000..296d03096 --- /dev/null +++ b/test/case/infix_containers/container_firewall_basic/Readme.adoc @@ -0,0 +1,54 @@ +=== Basic Firewall Container +==== Description +Verify that an nftables container can be used for IP masquerading and +port forwarding to another container running a basic web server. + +.... + <--- Docker containers ---> +.-------------. .----------------------. .--------..---------------. +| | mgmt |------------| mgmt | | | | fire || | web | +| host | data |------------| ext0 | target | int0 | | wall || eth0 | server | +'-------------'.42 .1'----------------------' '--------''---------------' + \ .1 .2 / + 192.168.0.0/24 \ 10.0.0.0/24 / + `-- VETH pair --' +.... + +The web server container is connected to the target on an internal +network, using a VETH pair, serving HTTP on port 91. + +The firewall container sets up a port forward with IP masquerding +to/from `ext0:8080` to 10.0.0.2:91. + +Correct operation is verified using HTTP GET requests for internal port +91 and external port 8080, to ensure the web page, with a known key +phrase, is only reachable from the public interface `ext0`, on +192.168.0.1:8080. + +==== Topology +ifdef::topdoc[] +image::../../test/case/infix_containers/container_firewall_basic/topology.png[Basic Firewall Container topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::container_firewall_basic/topology.png[Basic Firewall Container topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.png[Basic Firewall Container topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUT +. Set hostname to 'container-host' +. Create VETH pair for web server container +. Create firewall container from bundled OCI image +. Create web server container from bundled OCI image +. Verify firewall container has started +. Verify web container has started +. Verify connectivity, host can reach target:ext0 +. Verify 'web' is NOT reachable on http://container-host.local:91 +. Verify 'web' is reachable on http://container-host.local:8080 + + +<<< + diff --git a/test/case/infix_containers/container_firewall_basic/test.py b/test/case/infix_containers/container_firewall_basic/test.py new file mode 100755 index 000000000..0f1c9d0a9 --- /dev/null +++ b/test/case/infix_containers/container_firewall_basic/test.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +r"""Basic Firewall Container + +Verify that an nftables container can be used for IP masquerading and +port forwarding to another container running a basic web server. + +.... + <--- Docker containers ---> +.-------------. .----------------------. .--------..---------------. +| | mgmt |------------| mgmt | | | | fire || | web | +| host | data |------------| ext0 | target | int0 | | wall || eth0 | server | +'-------------'.42 .1'----------------------' '--------''---------------' + \ .1 .2 / + 192.168.0.0/24 \ 10.0.0.0/24 / + `-- VETH pair --' +.... + +The web server container is connected to the target on an internal +network, using a VETH pair, serving HTTP on port 91. + +The firewall container sets up a port forward with IP masquerding +to/from `ext0:8080` to 10.0.0.2:91. + +Correct operation is verified using HTTP GET requests for internal port +91 and external port 8080, to ensure the web page, with a known key +phrase, is only reachable from the public interface `ext0`, on +192.168.0.1:8080. + +""" +import infamy +from infamy.util import until, to_binary + + +with infamy.Test() as test: + NFTABLES = f"oci-archive:{infamy.Container.NFTABLES_IMAGE}" + HTTPD = f"oci-archive:{infamy.Container.HTTPD_IMAGE}" + WEBIP = "10.0.0.2" + INTIP = "10.0.0.1" + EXTIP = "192.168.0.1" + OURIP = "192.168.0.42" + WEBNM = "web" + NFTNM = "firewall" + GOOD_URL = f"http://{EXTIP}:8080/index.html" + BAD_URL = f"http://{EXTIP}:91/index.html" + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, ext0 = env.ltop.xlate("target", "ext0") + _, hport = env.ltop.xlate("host", "data") + addr = target.get_mgmt_ip() + + if not target.has_model("infix-containers"): + test.skip() + + with test.step("Set hostname to 'container-host'"): + target.put_config_dict("ietf-system", { + "system": { + "hostname": "container-host" + } + }) + + with test.step("Create VETH pair for web server container"): + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [ + { + "name": f"{ext0}", + "ipv4": { + "forwarding": True, + "address": [{ + "ip": f"{EXTIP}", + "prefix-length": 24 + }] + } + }, + { + "name": "int0", + "type": "infix-if-type:veth", + "enabled": True, + "infix-interfaces:veth": { + "peer": f"{WEBNM}" + }, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": f"{INTIP}", + "prefix-length": 24, + }] + } + }, + { + "name": f"{WEBNM}", + "type": "infix-if-type:veth", + "enabled": True, + "infix-interfaces:veth": { + "peer": "int0" + }, + "ipv4": { + "address": [{ + "ip": f"{WEBIP}", + "prefix-length": 24, + }] + }, + "container-network": {} + }, + ] + } + }) + + with test.step("Create firewall container from bundled OCI image"): + # Store the nftables .conf file contents as a multi-line string + config = to_binary(f"""#!/usr/sbin/nft -f + +flush ruleset + +define WAN = "{ext0}" +define INT = "int0" +define WIP = "{WEBIP}" + """ + """ + +table ip nat { + chain prerouting { + type nat hook prerouting priority 0; policy accept; + iifname $WAN tcp dport 8080 dnat to $WIP:91 + } + + chain postrouting { + type nat hook postrouting priority 100; policy accept; + oifname $WAN masquerade + oifname $INT masquerade + } +} +""") + + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{NFTNM}", + "image": f"{NFTABLES}", + "network": { + "host": True + }, + "mount": [ + { + "name": "nftables.conf", + "content": config, + "target": "/etc/nftables.conf" + } + ], + "privileged": True + } + ] + } + }) + + with test.step("Create web server container from bundled OCI image"): + target.put_config_dict("infix-containers", { + "containers": { + "container": [ + { + "name": f"{WEBNM}", + "image": f"{HTTPD}", + "command": "/usr/sbin/httpd -f -v -p 91", + "network": { + "interface": [ + {"name": f"{WEBNM}"} + ] + } + } + ] + } + }) + + with test.step("Verify firewall container has started"): + c = infamy.Container(target) + until(lambda: c.running(NFTNM), attempts=10) + + with test.step("Verify web container has started"): + c = infamy.Container(target) + until(lambda: c.running(WEBNM), attempts=10) + + with infamy.IsolatedMacVlan(hport) as ns: + NEEDLE = "tiny web server from the curiOS docker" + ns.addip(OURIP) + with test.step("Verify connectivity, host can reach target:ext0"): + ns.must_reach(EXTIP) + with test.step("Verify 'web' is NOT reachable on http://container-host.local:91"): + url = infamy.Furl(BAD_URL) + until(lambda: not url.nscheck(ns, NEEDLE)) + with test.step("Verify 'web' is reachable on http://container-host.local:8080"): + url = infamy.Furl(GOOD_URL) + until(lambda: url.nscheck(ns, NEEDLE)) + + test.succeed() diff --git a/test/case/infix_containers/container_firewall_basic/topology.dot b/test/case/infix_containers/container_firewall_basic/topology.dot new file mode 100644 index 000000000..19ab78607 --- /dev/null +++ b/test/case/infix_containers/container_firewall_basic/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 | {