diff --git a/README.md b/README.md index 1d58c3c..4e992ba 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ services: - UFW_MANAGED=true - UFW_FROM=192.168.0.0/24 - UFW_DENY_OUTGOING=true - - UFW_TO=192.168.1.2;192.168.2.0/24 + - UFW_TO=192.168.1.0/24;192.168.2.2:9000;192.168.2.3:8000/udp;192.168.3.0/24:tcp ports: - 80:80 ``` @@ -207,8 +207,10 @@ To Action From 172.17.0.2 80/tcp ALLOW FWD 192.168.0.0/24 <= this entry allows only 192.168.1.0/24 to access nginx server 192.168.0.0/24 ALLOW FWD 172.17.0.2 80/tcp <= this entry enables nginx server to reply back -192.168.1.2 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.1.2 ip for all tcp and udp ports -192.168.2.0/24 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.0/24 subnet for all tcp and udp ports +192.168.1.0/24 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.1.0/24 subnet for all tcp and udp ports +192.168.2.2 8000/udp ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.2 ip on 8000 port for udp protocol +192.168.2.3 9000 ALLOW FWD 172.17.0.2 <= this entry allow outgoing traffic to 192.168.2.3 ip on 9000 port for tcp and udp protocols +192.168.3.0/24/tcp ALLOW FWD 172.17.0.2/tcp <= this entry allow outgoing traffic to 192.168.3.0/24 subnet for all tcp ports Anywhere DENY FWD 172.17.0.2 <= this entry block any other outgoing requests ``` diff --git a/src/ufw-docker-automated.py b/src/ufw-docker-automated.py index 2e7e049..5c681a1 100644 --- a/src/ufw-docker-automated.py +++ b/src/ufw-docker-automated.py @@ -1,10 +1,52 @@ #!/usr/bin/env python -import subprocess +import re import docker +import subprocess from ipaddress import ip_network client = docker.from_env() +# implementation of a get method ontop __builtins__.list class +class _list(__builtins__.list): + def get(self, index, default=None): + try: + return self[index] if self[index] else default + except IndexError: + return default + +def to_string_port(port): + if port.get(0) and port.get(1): + return f"on port {int(port.get(0))}/{port.get(1)}" + elif port.get(0): + return f"on port {int(port.get(0))}" + elif port.get(1): + return f"on proto {port.get(1)}" + else: + return "" + +def validate_port(port): + if not port: + return {} + r = re.compile(r'^(\d+)?((/|^)(tcp|udp))?$') + if r.match(port) is None: + raise ValueError(f"'{port}' does not appear to be a valid port and protocol (examples: '80/tcp' or 'udp')") + if port in ['tcp', 'udp']: + return {'protocol': port, 'to_string_port': to_string_port(_list([None, port]))} + port_and_protocol_split = _list(port.split('/')) + if not (1 <= int(port_and_protocol_split.get(0)) <= 65535): + raise ValueError(f"'{port}' does not appear to be a valid port number") + return {'port': int(port_and_protocol_split.get(0)), 'protocol': port_and_protocol_split.get(1), 'to_string_port': to_string_port(port_and_protocol_split)} + +def parse_ufw_to(label): + output = [] + for item in label.split(';'): + item_list = _list(item.split(':')) + if len(item_list) == 2 or len(item_list) == 1: + output += [{ + **{'ipnet': ip_network(item_list.get(0))}, + **validate_port(port=item_list.get(1)) + }] + return output def manage_ufw(): for event in client.events(decode=True): @@ -51,7 +93,7 @@ def manage_ufw(): if ufw_deny_outgoing == 'True' and 'UFW_TO' in container.labels: try: - ufw_to = [ip_network(ipnet) for ipnet in container.labels.get('UFW_TO').split(';') if ipnet] + ufw_to = parse_ufw_to(container.labels.get('UFW_TO')) except ValueError as e: print(f"ufw-docker-automated: Invalid UFW label: UFW_TO={container.labels.get('UFW_TO')} exception={e}") ufw_to = None @@ -83,8 +125,12 @@ def manage_ufw(): if ufw_to: for destination in ufw_to: # Allow outgoing requests from the container to whitelisted IPs or Subnets - print(f"ufw-docker-automated: Adding UFW rule: allow outgoing from container {container.name} to {destination}") - subprocess.run([f"ufw route allow from {container_ip} to {destination}"], + print(f"ufw-docker-automated: Adding UFW rule: allow outgoing from container {container.name} to {destination.get('ipnet')} {destination.get('to_string_port', '')}") + destination_port = f"port {destination.get('port')}" if destination.get('port') else "" + destination_protocol = f"proto {destination.get('protocol')}" if destination.get('protocol') else "" + subprocess.run([f"ufw route allow {destination_protocol} \ + from {container_ip} \ + to {destination.get('ipnet')} {destination_port}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) # Deny any other outgoing requests