Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

Initial support for UDP #123

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 1 addition & 1 deletion docs/developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ DOCKER_REGISTRY=$(minikube ip) ./local_dev/run_e2e_tests.sh
In order to run the unit tests:

```bash
python setup.py test --addopts="-m 'not e2e' --runslow"
coverage run setup.py test --addopts="-m 'not e2e' --runslow"
```

## Cleanup
Expand Down
2 changes: 1 addition & 1 deletion e2e-manifests/01-deny-all-traffic-to-an-application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ spec:
app: web
spec:
containers:
- image: nginx
- image: busybox
name: web
ports:
- containerPort: 80
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
01-deny-all-traffic-to-an-application:app=web:
01-deny-all-traffic-to-an-application:app=web:
-*:
-TCP/*:
success: true
-UDP/*:
success: true
8 changes: 4 additions & 4 deletions e2e-manifests/expected/labels-with-all-legal-characters.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
illuminatio-inverted-labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
-*:
-TCP/*:
success: true
labels-with-all-legal-characters:illuminatio-inverted-test.io/test-123_XYZ=test_456-123.ABC:
labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
-*:
-TCP/*:
success: true
illuminatio-inverted-labels-with-all-legal-characters:illuminatio-inverted-test.io/test-123_XYZ=test_456-123.ABC:
labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
-*:
-TCP/*:
success: true
labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC:
"*":
"TCP/*":
success: true
4 changes: 3 additions & 1 deletion e2e-manifests/expected/max-length-labels.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
max-length-labels:253-characters-or-less.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
max-length-labels:253-characters-or-less.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
-*:
-TCP/*:
success: true
-UDP/*:
success: true
16 changes: 16 additions & 0 deletions e2e-manifests/expected/udp-support.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
illuminatio-inverted-udp-support:app=bookstore:
udp-support:app=bookstore,role=api:
-UDP/*:
success: true
illuminatio-inverted-udp-support:illuminatio-inverted-app=bookstore:
udp-support:app=bookstore,role=api:
-UDP/*:
success: true
udp-support:app=bookstore:
udp-support:app=bookstore,role=api:
UDP/*:
success: true
udp-support:illuminatio-inverted-app=bookstore:
udp-support:app=bookstore,role=api:
-UDP/*:
success: true
79 changes: 79 additions & 0 deletions e2e-manifests/udp-support.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
apiVersion: v1
kind: Namespace
metadata:
name: udp-support
labels:
illuminatio-e2e: udp-support
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: bookstore
role: api
illuminatio-e2e: udp-support
name: apiserver
namespace: udp-support
spec:
selector:
matchLabels:
app: bookstore
role: api
template:
metadata:
labels:
app: bookstore
role: api
spec:
containers:
- image: busybox
command:
- "nc"
args:
- "-l"
- "-u"
- "0.0.0.0"
- "80"
name: apiserver
ports:
- containerPort: 80
protocol: UDP
---
apiVersion: v1
kind: Service
metadata:
labels:
app: bookstore
role: api
illuminatio-e2e: udp-support
name: apiserver
namespace: udp-support
spec:
ports:
- port: 80
protocol: UDP
targetPort: 80
selector:
app: bookstore
role: api
type: ClusterIP
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: api-allow
labels:
illuminatio-e2e: udp-support
namespace: udp-support
spec:
podSelector:
matchLabels:
app: bookstore
role: api
ingress:
- from:
- podSelector:
matchLabels:
app: bookstore
ports:
- protocol: UDP
13 changes: 7 additions & 6 deletions src/illuminatio/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,13 @@ def __init__(self, namespace_labels, pod_labels):
self.pod_labels = pod_labels

def __eq__(self, other):
if isinstance(other, GenericClusterHost):
return (
self.namespace_labels == other.namespace_labels
and self.pod_labels == other.pod_labels
)
return False
if not isinstance(other, GenericClusterHost):
return False

return (
self.namespace_labels == other.namespace_labels
and self.pod_labels == other.pod_labels
)

def __hash__(self):
return hash(str(self))
Expand Down
7 changes: 3 additions & 4 deletions src/illuminatio/illuminatio.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ def generate(outfile: str):
@click.option(
"-t",
"--target-image",
default="nginx:stable",
help="Target image that is used to generate pods (should have a webserver inside listening on port 80)",
default="busybox",
help="Target image that is used to generate pods (should be a busybox or something with nc)",
)
@click.option(
"-c",
Expand Down Expand Up @@ -197,8 +197,7 @@ def execute_tests(cases, orch, cri_socket):
"""
orch.test_cases = cases
core_api = k8s.client.CoreV1Api()
# namespace should be an argument !
# -> illuminatio
# TODO: namespace should be an argument !
namespace_name = "illuminatio"

if not orch.namespace_exists(namespace_name, core_api):
Expand Down
56 changes: 35 additions & 21 deletions src/illuminatio/illuminatio_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,35 @@ def run_tests_for_target(network_ns, ports, target):
# https://stackoverflow.com/questions/2805231/how-can-i-do-dns-lookups-in-python-including-referring-to-etc-hosts
LOGGER.info("Target: %s", target)
port_on_nums = {port.replace("-", ""): port for port in ports}
port_string = ",".join(port_on_nums.keys())
# TODO extra method + tests
tcp_ports = list()
udp_ports = list()

for port in port_on_nums.keys():
port_num = port.split("/")[1]
# starts_with !
if port.startswith("UDP"):
udp_ports.append(port_num)
elif port.startswith("TCP"):
tcp_ports.append(port_num)
else:
LOGGER.error(f"Unsupported protocol: {port}")

tcp_port_string = ",".join(tcp_ports)
udp_port_string = ",".join(udp_ports)

ipv6_arg = ""
if ipaddress.ip_address(target).version == 6:
ipv6_arg = "-6"

nm_scanner = nmap.PortScanner()
with Namespace(network_ns, "net"):
nm_scanner.scan(target, arguments=f"-n -Pn -p {port_string} {ipv6_arg}")
if len(tcp_port_string) > 0:
nm_scanner.scan(target, arguments=f"-n -Pn -p {tcp_port_string} {ipv6_arg}")
if len(udp_port_string) > 0:
nm_scanner.scan(
target, arguments=f"-n -Pn -sU -p {udp_port_string} {ipv6_arg}"
)
LOGGER.info("Ran nmap with cmd %s", nm_scanner.command_line())

return extract_results_from_nmap(nm_scanner, port_on_nums, target)
Expand All @@ -183,6 +203,7 @@ def extract_results_from_nmap(nmap_res, port_on_nums, target):
"""
Extracts the results of an nmap scan into a dictionary
"""

hosts = nmap_res.all_hosts()
if len(hosts) != 1:
port_string = ",".join(port_on_nums.keys())
Expand All @@ -204,34 +225,27 @@ def extract_results_from_nmap(nmap_res, port_on_nums, target):
state = nmap_res[host].tcp(port)["state"]
else:
state = nmap_res[host][proto][port]["state"]
port_with_expectation = port_on_nums[str(port)]
should_be_blocked = "-" in port_with_expectation
was_blocked = state == "filtered"

port_with_expectation = port_on_nums[f"{proto.upper()}/{port}"]
should_be_blocked = port_with_expectation.startswith("-")
was_blocked = False
if "filtered" in state:
# For UDP we can not say if the port is really blocked
# or the packet never arrvied so we get
# 'nmap-state': 'open|filtered'
was_blocked = True

results[port_with_expectation] = {
"success": should_be_blocked == was_blocked,
"string": build_result_string(
port, target, should_be_blocked, was_blocked
f"{proto.upper()}/{port}", target, should_be_blocked, was_blocked
),
"nmap-state": state,
}

return results


def get_domain_name_for(host_string):
"""
Replaces namespace:serviceName syntax with serviceName.namespace one,
appending default as namespace if None exists
"""
return ".".join(
reversed(
("%s%s" % (("" if ":" in host_string else "default:"), host_string)).split(
":"
)
)
)


def get_docker_network_namespace(pod_namespace, pod_name):
"""
Fetches and retrieves the network namespace information
Expand Down Expand Up @@ -313,7 +327,7 @@ def get_cri_network_namespace(host_namespace, host_name):
)
LOGGER.error(prc1.stderr)
pod_id = prc1.stdout.strip()
# ToDo error handling
# TODO: error handling
cmd2 = ["crictl", "inspectp", pod_id]
prc2 = subprocess.run(cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if prc2.returncode:
Expand Down
15 changes: 9 additions & 6 deletions src/illuminatio/k8s_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def create_pod_manifest(host: Host, additional_labels, generate_name, container)


def create_service_manifest(
host: Host, additional_selector_labels, svc_labels, port_nums
host: Host, additional_selector_labels, svc_labels, target_ports
):
"""
Creates and returns a service manifest with given parameters
Expand All @@ -104,11 +104,14 @@ def create_service_manifest(
svc.spec.selector[key] = value
svc.metadata.labels = svc_labels
validate_cleanup_in(svc.metadata.labels)
# TODO: support for other protocols missing, target port might not work like that for multiple ports
ports = [
k8s.client.V1ServicePort(protocol="TCP", port=portNum, target_port=80)
for portNum in port_nums
]
ports = []
for target_port in target_ports:
protocol, port = target_port.split("/")
# Replace '/' with '-' to be DNS-1123 conformant
# and convert to lower case
name = target_port.replace("/", "-").lower()
ports.append(k8s.client.V1ServicePort(name=name, protocol=protocol, port=int(port), target_port=80))

svc.spec.ports = ports
return svc

Expand Down
25 changes: 24 additions & 1 deletion src/illuminatio/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def from_network_policy(cls, net_pol: k8s.client.V1NetworkPolicy):
"""
Returns a class containing the concerns and rules of a given NetworkPolicy
"""

#TODO must be fixed here
concerns = {NAMESPACE: net_pol.metadata.namespace}
if net_pol.spec.pod_selector.match_labels is not None:
concerns[POD_SELECTOR_LABELS] = net_pol.spec.pod_selector.match_labels
Expand All @@ -105,13 +107,34 @@ def from_network_policy(cls, net_pol: k8s.client.V1NetworkPolicy):
return cls(concerns, allowed)


def _generate_port_list(ports: list()) -> list():
if ports is not None:
port_list = list()
for port in ports:
protocol = "TCP"
target_port = MATCH_ALL_WILDCARD
if port.protocol is not None:
protocol = port.protocol

if port.port is not None:
target_port = port.port

port_list.append(f"{protocol}/{target_port}")
else:
port_list = [f"TCP/{MATCH_ALL_WILDCARD}", f"UDP/{MATCH_ALL_WILDCARD}"]

return port_list


# helper function
def build_connections(verb, target, ports):
"""
Helper function to build Connection tuples
"""
out = []
port_list = [p.port for p in ports] if (ports is not None) else [MATCH_ALL_WILDCARD]

# TODO fix here
port_list = _generate_port_list(ports)
if target is not None:
for item in target:
if item.ip_block is not None:
Expand Down
18 changes: 11 additions & 7 deletions src/illuminatio/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ def __init__(self, from_host: Host, to_host: Host, on_port, should_connect):
raise ValueError("shouldConnect may not be None")
self.from_host = from_host
self.to_host = to_host
# on_port can be None, which matches all ports
self._on_port = on_port if on_port else "*"
# on_port can be None, which matches all ports on the default protocol
self._on_port = on_port if on_port else "TCP/*"
self._should_connect = should_connect
self.port_string = "%s%s" % (
("" if self._should_connect else "-"),
str(self._on_port),
)

prefix = ""
if not self._should_connect:
prefix = "-"
self.port_string = f"{prefix}{self._on_port}"

# TODO adjust compare?
# At index 0 diff:
# NetworkTestCase(from=GenericClusterHost(namespaceLabels={}, podLabels={}), to=ClusterHost(namespace=default, podLabels={}), port=-31203) !=
# NetworkTestCase(from=GenericClusterHost(namespaceLabels={}, podLabels={}), to=ClusterHost(namespace=default, podLabels={}), port=TCP/*)
def __eq__(self, other):
if isinstance(other, NetworkTestCase):
return (
Expand Down
Loading