Skip to content

Commit

Permalink
Merge pull request #754 from kernelkit/firewall-test
Browse files Browse the repository at this point in the history
Container firewall test
  • Loading branch information
troglobit authored Oct 23, 2024
2 parents 9f33658 + c7760a6 commit d36296d
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 16 deletions.
10 changes: 10 additions & 0 deletions test/case/infix_containers/Readme.adoc
Original file line number Diff line number Diff line change
@@ -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[]
Expand All @@ -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[]
4 changes: 2 additions & 2 deletions test/case/infix_containers/container_basic/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions test/case/infix_containers/container_bridge/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<html><body><p>Kilroy was here</p></body></html>"
Expand All @@ -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": [
Expand Down Expand Up @@ -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"
}
],
Expand Down
54 changes: 54 additions & 0 deletions test/case/infix_containers/container_firewall_basic/Readme.adoc
Original file line number Diff line number Diff line change
@@ -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 changes: 197 additions & 0 deletions test/case/infix_containers/container_firewall_basic/test.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 24 additions & 0 deletions test/case/infix_containers/container_firewall_basic/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!",
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 "]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions test/case/infix_containers/container_phys/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

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"
Expand Down Expand Up @@ -51,7 +50,7 @@
"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": [
Expand Down
2 changes: 1 addition & 1 deletion test/case/infix_containers/container_veth/Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
'-------------' '---------------' '--------'
| /
Expand Down
Loading

0 comments on commit d36296d

Please sign in to comment.