mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-29 04:03:01 +02:00
Merge pull request #754 from kernelkit/firewall-test
Container firewall test
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user