Merge pull request #754 from kernelkit/firewall-test

Container firewall test
This commit is contained in:
Joachim Wiberg
2024-10-23 13:42:53 +02:00
committed by GitHub
13 changed files with 313 additions and 16 deletions
+10
View File
@@ -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[]
@@ -20,7 +20,7 @@ from infamy.util import until
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 @@ with infamy.Test() as test:
"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
@@ -16,11 +16,10 @@ port accessed from the host.
"""
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 = "<html><body><p>Kilroy was here</p></body></html>"
@@ -35,7 +34,7 @@ with infamy.Test() as test:
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 @@ with infamy.Test() as test:
"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"
}
],
@@ -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
<<<
+197
View File
@@ -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()
@@ -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!",
kind="controller",
];
target [
label="{ <mgmt> mgmt | <ext0> ext0 } | target",
pos="10,12!",
kind="infix",
];
host:mgmt -- target:mgmt [kind=mgmt, color=lightgrey]
host:data -- target:ext0 [color=black, headlabel=".1 ", taillabel=" .2", label="\n 192.168.0.1/24 "]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -15,7 +15,6 @@ from infamy.util import until
with infamy.Test() as test:
NAME = "web-phys"
IMAGE = "curios-httpd-edge.tar.gz"
DUTIP = "10.0.0.2"
OURIP = "10.0.0.1"
URL = f"http://{DUTIP}:91/index.html"
@@ -51,7 +50,7 @@ with infamy.Test() as test:
"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": {
"interface": [
@@ -5,7 +5,7 @@ regular bridge, a VETH pair connects the container to the bridge.
....
.-------------. .---------------. .--------.
| | tgt |---------| mgmt | | | web- |
| | mgmt |---------| mgmt | | | web- |
| host | data |---------| data | target | | server |
'-------------' '---------------' '--------'
| /
@@ -11,7 +11,7 @@ regular bridge, a VETH pair connects the container to the bridge.
....
.-------------. .---------------. .--------.
| | tgt |---------| mgmt | | | web- |
| | mgmt |---------| mgmt | | | web- |
| host | data |---------| data | target | | server |
'-------------' '---------------' '--------'
| /
@@ -26,7 +26,6 @@ from infamy.util import until
with infamy.Test() as test:
NAME = "web-br0-veth"
IMAGE = "curios-httpd-edge.tar.gz"
DUTIP = "10.0.0.2"
OURIP = "10.0.0.1"
URL = f"http://{DUTIP}:91/index.html"
@@ -85,7 +84,7 @@ with infamy.Test() as test:
"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": {
"interface": [
@@ -11,3 +11,7 @@
- name: container_veth
case: container_veth/test.py
- name: container_firewall_basic
case: container_firewall_basic/test.py
+2 -1
View File
@@ -5,7 +5,8 @@ from infamy.util import warn
class Container:
"""Helper methods"""
IMAGE = "curios-httpd-v24.05.0.tar.gz"
HTTPD_IMAGE = "curios-httpd-v24.05.0.tar.gz"
NFTABLES_IMAGE = "curios-nftables-v24.05.0.tar.gz"
def __init__(self, target):
self.system = target
+12 -2
View File
@@ -1,8 +1,10 @@
"""Helper functions for tests"""
import base64
import time
import threading
import infamy.neigh
import infamy.netconf as netconf
import infamy.restconf as restconf
from infamy import netconf
from infamy import restconf
class ParallelFn(threading.Thread):
@@ -59,6 +61,14 @@ def is_reachable(neigh, env, pwd):
return netconf_reachable and restconf_reachable
def to_binary(text):
"""Base64 encode the text, removing newlines"""
enc = base64.b64encode(text.encode('utf-8'))
# Convert the encoded bytes to a string and remove any newlines
return enc.decode('utf-8').replace('\n', '')
def wait_boot(target, env):
print(f"{target} is shutting down ...")
until(lambda: not target.reachable(), attempts=100)