diff --git a/docker-compose.yml b/docker-compose.yml index 965e815e..036b7aa1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,10 @@ services: image: "wireguard.wireguard.dnp.dappnode.eth:0.1.1" container_name: DAppNodeCore-wireguard.wireguard.dnp.dappnode.eth restart: unless-stopped + # Keep Docker aliases while using Dappnode DNS for external lookups. + dns: + - 172.33.1.2 + - 10.20.0.2 cap_add: - NET_ADMIN - SYS_MODULE diff --git a/readme-vars.yml b/readme-vars.yml index 643bce8e..3946d7af 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -158,6 +158,7 @@ init_diagram: | "wireguard:latest" <- Base Images # changelog changelogs: + - {date: "24.10.26:", desc: "Use Dappnode DNS upstreams and ignore outgoing DNS sockets when detecting CoreDNS port conflicts."} - {date: "24.01.26:", desc: "Rebase to Alpine 3.23 again as openresolv alpine 3.23 package has now been updated."} - {date: "22.01.26:", desc: "Revert to Alpine 3.22 due to resolvconf bug."} - {date: "04.01.26:", desc: "Rebase to Alpine 3.23."} diff --git a/root/etc/s6-overlay/s6-rc.d/svc-coredns/run b/root/etc/s6-overlay/s6-rc.d/svc-coredns/run index f9a478c3..aeda5fcf 100755 --- a/root/etc/s6-overlay/s6-rc.d/svc-coredns/run +++ b/root/etc/s6-overlay/s6-rc.d/svc-coredns/run @@ -1,7 +1,7 @@ #!/usr/bin/with-contenv bash # shellcheck shell=bash -if netstat -apn | grep -q ":53 " && [[ -z "${USE_COREDNS}" ]]; then +if netstat -lntup | grep -q ":53 " && [[ -z "${USE_COREDNS}" ]]; then USE_COREDNS="false" fi diff --git a/tests/test_coredns_port_detection.py b/tests/test_coredns_port_detection.py new file mode 100644 index 00000000..82bc9549 --- /dev/null +++ b/tests/test_coredns_port_detection.py @@ -0,0 +1,59 @@ +"""Exercise the image's listener guard with real sockets and no VPN state.""" +import argparse +import json +from pathlib import Path +import subprocess +import time +import uuid + +parser = argparse.ArgumentParser() +parser.add_argument('--image', required=True) +parser.add_argument('--helper-image', default='node:22.23.4-alpine3.24') +parser.add_argument('--output') +args = parser.parse_args() +name = 'wireguard-port-check-' + uuid.uuid4().hex[:10] +holder = name + '-udp' + +def docker(*cmd, check=True): + result = subprocess.run(['docker', *cmd], capture_output=True, text=True, timeout=30) + if check and result.returncode: + raise RuntimeError('Docker ' + cmd[0] + ' failed: ' + result.stderr[-500:]) + return result + +try: + docker('run', '-d', '--name', name, '--network', 'none', '--cpus', '1', '--memory', '256m', + '--entrypoint', 'sh', args.image, '-c', 'sleep infinity') + script = docker('exec', name, 'cat', '/etc/s6-overlay/s6-rc.d/svc-coredns/run').stdout + line = next(line.strip() for line in script.splitlines() if line.startswith('if netstat ')) + guard = line.removeprefix('if ').split(' && [[', 1)[0] + docker('run', '-d', '--name', holder, '--network', 'container:' + name, '--cpus', '1', '--memory', '128m', + '--entrypoint', 'node', args.helper_image, '-e', + "const s=require('dgram').createSocket('udp4');s.connect(53,'127.0.0.1',()=>console.log('connected'));setInterval(()=>{},10000)") + for _ in range(30): + if 'connected' in docker('logs', holder).stdout: + break + time.sleep(.1) + else: + raise RuntimeError('UDP fixture did not become ready') + # Reproduce the original false positive while there is no listening server. + assert docker('exec', name, 'sh', '-c', 'netstat -apn | grep -q ":53 "', check=False).returncode == 0 + assert docker('exec', name, 'sh', '-c', guard, check=False).returncode == 1, 'Outgoing DNS socket was mistaken for a listener' + # Then start a real listener. The same image-provided guard must detect it. + docker('exec', name, 'sh', '-c', "printf '.:53 {\\n errors\\n}\\n' > /tmp/port-check-Corefile") + docker('exec', '-d', name, '/usr/bin/coredns', '-conf', '/tmp/port-check-Corefile') + for _ in range(30): + if docker('exec', name, 'sh', '-c', guard, check=False).returncode == 0: + break + time.sleep(.1) + else: + raise AssertionError('Actual DNS listener was not detected') + result = {'success': True, 'oldCheckFalsePositiveReproduced': True, + 'outgoingDnsSocketIgnored': True, 'actualDnsListenerDetected': True, + 'productionStateMounted': False, 'network': 'none'} +finally: + docker('rm', '-f', '-v', holder, check=False) + docker('rm', '-f', '-v', name, check=False) + +if args.output: + Path(args.output).write_text(json.dumps(result, indent=2) + '\n') +print(json.dumps(result, indent=2))