up
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
сети, проверяет TLS, маршруты, перенаправления и перезапуск, затем удаляет
|
||||
свои контейнеры и сеть. Использует локальный `nginx:alpine` и временный тестовый CA;
|
||||
работающие окружения, production и системное хранилище доверия не меняет.
|
||||
- `dns.py --docker` — проверяет оба локальных домена по UDP/TCP и отсутствие
|
||||
внешних IPv6-ответов в отдельной внутренней Docker-сети без WAN до и после
|
||||
перезапуска CoreDNS. `--server <IP ноутбука>` проверяет работающий DNS.
|
||||
|
||||
Команды из корня проекта:
|
||||
|
||||
@@ -21,4 +24,6 @@ python3 tests/venue/deploy.py
|
||||
python3 tests/venue/smoke.py --verify
|
||||
python3 tests/venue/https.py
|
||||
python3 tests/venue/https.py --docker
|
||||
python3 tests/venue/dns.py --docker
|
||||
python3 tests/venue/dns.py --server 192.168.10.155
|
||||
```
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Verify venue DNS over UDP/TCP; --docker tests an isolated server without WAN."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
DOMAINS = ['evening-detective.crabs-games.art', 'evening-detective-api.crabs-games.art']
|
||||
IMAGE = 'coredns/coredns:1.14.7'
|
||||
|
||||
|
||||
def query(server, domain, kind=1, tcp=False):
|
||||
identifier = os.urandom(2)
|
||||
question = b''.join(bytes([len(label)]) + label.encode() for label in domain.split('.')) + b'\0'
|
||||
packet = identifier + struct.pack('!5H', 0x0100, 1, 0, 0, 0) + question + struct.pack('!2H', kind, 1)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM if tcp else socket.SOCK_DGRAM) as connection:
|
||||
connection.settimeout(3)
|
||||
connection.connect((server, 53))
|
||||
if tcp:
|
||||
connection.sendall(struct.pack('!H', len(packet)) + packet)
|
||||
|
||||
def receive(size):
|
||||
data = b''
|
||||
while len(data) < size:
|
||||
chunk = connection.recv(size - len(data))
|
||||
if not chunk:
|
||||
raise RuntimeError('Incomplete DNS response')
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
response = receive(struct.unpack('!H', receive(2))[0])
|
||||
else:
|
||||
connection.send(packet)
|
||||
response = connection.recv(4096)
|
||||
assert response[:2] == identifier, 'DNS response identifier mismatch'
|
||||
flags, questions, answers, _, _ = struct.unpack_from('!5H', response, 2)
|
||||
assert flags & 0x8000 and not flags & 0x0200, 'Invalid or truncated DNS response'
|
||||
|
||||
def skip_name(offset):
|
||||
while response[offset]:
|
||||
if response[offset] & 0xc0 == 0xc0:
|
||||
return offset + 2
|
||||
offset += response[offset] + 1
|
||||
return offset + 1
|
||||
|
||||
offset = 12
|
||||
for _ in range(questions):
|
||||
offset = skip_name(offset) + 4
|
||||
addresses = []
|
||||
for _ in range(answers):
|
||||
offset = skip_name(offset)
|
||||
record_type, _, _, length = struct.unpack_from('!HHIH', response, offset)
|
||||
offset += 10
|
||||
if record_type in (1, 28):
|
||||
addresses.append(socket.inet_ntop(socket.AF_INET if record_type == 1 else socket.AF_INET6,
|
||||
response[offset:offset + length]))
|
||||
offset += length
|
||||
return flags & 15, addresses
|
||||
|
||||
|
||||
def verify(server, expected):
|
||||
for domain in DOMAINS:
|
||||
for tcp in (False, True):
|
||||
assert query(server, domain, tcp=tcp) == (0, [expected]), domain
|
||||
assert query(server, domain, kind=28, tcp=tcp) == (0, []), 'Unexpected external IPv6 answer: ' + domain
|
||||
print('PASS: both venue domains resolve locally over UDP/TCP, no external IPv6 answers')
|
||||
|
||||
|
||||
def docker_test():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
network = 'venue-dns-test-' + uuid.uuid4().hex[:10]
|
||||
container = network + '-server'
|
||||
|
||||
def run(*args):
|
||||
return subprocess.check_output(args, text=True, stderr=subprocess.PIPE).strip()
|
||||
|
||||
try:
|
||||
run('docker', 'network', 'create', '--internal', network)
|
||||
run('docker', 'run', '-d', '--name', container, '--network', network,
|
||||
'-e', 'VENUE_DNS_IP=192.0.2.55', '-v', str(root / 'venue/Corefile') + ':/etc/coredns/Corefile:ro',
|
||||
IMAGE, '-conf', '/etc/coredns/Corefile')
|
||||
address = run('docker', 'inspect', '--format', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', container)
|
||||
for attempt in range(30):
|
||||
try:
|
||||
query(address, DOMAINS[0])
|
||||
break
|
||||
except (OSError, RuntimeError):
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise AssertionError('DNS did not start')
|
||||
verify(address, '192.0.2.55')
|
||||
assert run('docker', 'network', 'inspect', '--format', '{{.Internal}}', network) == 'true'
|
||||
run('docker', 'restart', container)
|
||||
for attempt in range(30):
|
||||
try:
|
||||
query(address, DOMAINS[0])
|
||||
break
|
||||
except (OSError, RuntimeError):
|
||||
time.sleep(0.1)
|
||||
verify(address, '192.0.2.55')
|
||||
print('PASS: CoreDNS works without WAN before and after restart')
|
||||
finally:
|
||||
subprocess.run(['docker', 'rm', '-f', container], capture_output=True)
|
||||
subprocess.run(['docker', 'network', 'rm', network], capture_output=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--docker', action='store_true')
|
||||
parser.add_argument('--server', help='Laptop IPv4 address')
|
||||
args = parser.parse_args()
|
||||
if args.docker:
|
||||
docker_test()
|
||||
elif args.server:
|
||||
verify(args.server, args.server)
|
||||
else:
|
||||
parser.error('Use --docker or --server <laptop IPv4>')
|
||||
@@ -103,6 +103,7 @@ with tempfile.TemporaryDirectory(prefix="venue-https-") as directory:
|
||||
assert prefix + " build --pull=false" in commands
|
||||
assert commands.index(prefix + " build --pull=false") < commands.index(prefix + " up -d")
|
||||
assert (prefix + " pull " in commands) == cold
|
||||
assert (prefix + " pull dns" in commands) == cold
|
||||
assert ("docker pull alpine:3.22" in commands) == cold
|
||||
assert "make " not in commands and "npm " not in commands
|
||||
else:
|
||||
@@ -173,6 +174,13 @@ with tempfile.TemporaryDirectory(prefix="venue-https-") as directory:
|
||||
assert len(configs) == 1 and configs[0]["source"].endswith("/nginx.https.conf") and configs[0]["read_only"]
|
||||
assert any(v["target"] == "/etc/nginx/certificates" and v["read_only"] for v in https["services"]["proxy"]["volumes"])
|
||||
assert https["networks"]["default"]["internal"] and https["volumes"] == http_config["volumes"]
|
||||
dns = https["services"]["dns"]
|
||||
assert dns["image"] == "coredns/coredns:1.14.7"
|
||||
assert dns["environment"]["VENUE_DNS_IP"] == "127.0.0.1"
|
||||
assert set(dns["networks"]) == {"entrance"}
|
||||
assert {(p["protocol"], str(p["published"])) for p in dns["ports"]} == {("udp", "53"), ("tcp", "53")}
|
||||
assert all(p["host_ip"] == "127.0.0.1" for p in dns["ports"])
|
||||
assert any(v["target"] == "/etc/coredns/Corefile" and v["read_only"] for v in dns["volumes"])
|
||||
print("PASS: Compose merge, HTTPS addresses, ports, read-only mounts and preserved data volumes")
|
||||
suffix = uuid.uuid4().hex[:10]
|
||||
network = "venue-https-test-" + suffix
|
||||
|
||||
Reference in New Issue
Block a user