115 lines
3.8 KiB
Python
Executable File
115 lines
3.8 KiB
Python
Executable File
#!/usr/bin/python3
|
|
"""Secure the ports Docker publishes, with rules in the DOCKER-USER chain.
|
|
|
|
Usage: docker-rules apply|remove
|
|
|
|
Docker evaluates DOCKER-USER in FORWARD before its own rules, which makes it the
|
|
place to filter traffic to containers. The rules live in their own chain,
|
|
VPN-ROUTER-DOCKER, entered by one jump at the top of DOCKER-USER, so anything
|
|
else in DOCKER-USER is left alone. Traffic arriving on the external interface is
|
|
accepted when it is IPsec protected, when it is TCP to docker.exposed_ports, or
|
|
when it belongs to an established connection; all other traffic from the
|
|
external interface is dropped. Traffic from other interfaces is not affected.
|
|
|
|
docker.mode decides whether this runs: auto applies the rules when the Docker
|
|
service is installed, on requires it, off removes the rules. The vpn-router-docker
|
|
unit runs apply after docker.service starts and remove when it stops.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
|
|
import vpnrouter
|
|
from vpnrouter import fail, warn
|
|
|
|
CHAIN = 'VPN-ROUTER-DOCKER'
|
|
USER_CHAIN = 'DOCKER-USER'
|
|
|
|
|
|
def iptables(*args, check=True):
|
|
result = subprocess.run(['iptables', *args], capture_output=True, text=True)
|
|
if check and result.returncode != 0:
|
|
fail(f'iptables {" ".join(args)} failed: {result.stderr.strip()}')
|
|
return result
|
|
|
|
|
|
def docker_installed():
|
|
"""True when systemd knows the docker.service unit."""
|
|
result = subprocess.run(
|
|
['systemctl', 'list-unit-files', '--no-legend', 'docker.service'],
|
|
capture_output=True, text=True)
|
|
return result.returncode == 0 and result.stdout.strip() != ''
|
|
|
|
|
|
def remove():
|
|
"""Take the jump and the chain out; nothing happens when they are absent."""
|
|
while iptables('-D', USER_CHAIN, '-j', CHAIN, check=False).returncode == 0:
|
|
pass
|
|
iptables('-F', CHAIN, check=False)
|
|
iptables('-X', CHAIN, check=False)
|
|
|
|
|
|
def apply(cfg):
|
|
mode = cfg['docker']['mode']
|
|
if mode == 'off':
|
|
remove()
|
|
return
|
|
|
|
if not docker_installed():
|
|
if mode == 'on':
|
|
fail('docker.mode is on but the docker service is not installed; '
|
|
'install Docker before vpn-router')
|
|
return
|
|
|
|
if iptables('-S', USER_CHAIN, check=False).returncode != 0:
|
|
if mode == 'on':
|
|
fail(f'docker.mode is on but there is no {USER_CHAIN} chain; '
|
|
'is the Docker daemon running?')
|
|
warn(f'no {USER_CHAIN} chain, the Docker daemon is not running; '
|
|
'the rules are applied when it starts')
|
|
return
|
|
|
|
ctx = vpnrouter.build_context(cfg)
|
|
wan = ctx['wan_iface']
|
|
if not wan:
|
|
fail('the external interface is not known; set interfaces.external '
|
|
'or use mode auto or interfaces')
|
|
|
|
ports = ','.join(item.strip() for item in cfg['docker']['exposed_ports'])
|
|
|
|
remove()
|
|
iptables('-N', CHAIN)
|
|
iptables('-A', CHAIN, '-i', wan, '-m', 'policy', '--dir', 'in', '--pol', 'ipsec', '-j', 'ACCEPT')
|
|
if ports:
|
|
iptables('-A', CHAIN, '-i', wan, '-p', 'tcp', '-m', 'multiport', '--dports', ports, '-j', 'ACCEPT')
|
|
iptables('-A', CHAIN, '-i', wan, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT')
|
|
iptables('-A', CHAIN, '-i', wan, '-j', 'DROP')
|
|
iptables('-I', USER_CHAIN, '1', '-j', CHAIN)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 2 or sys.argv[1] not in ('apply', 'remove'):
|
|
print('usage: docker-rules apply|remove', file=sys.stderr)
|
|
return 2
|
|
|
|
if sys.argv[1] == 'remove':
|
|
remove()
|
|
return 0
|
|
|
|
cfg = vpnrouter.load_config()
|
|
if vpnrouter.deferred(cfg):
|
|
return 0
|
|
try:
|
|
vpnrouter.validate(cfg)
|
|
apply(cfg)
|
|
except vpnrouter.ConfigError as exc:
|
|
fail(str(exc))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|