This commit is contained in:
2026-10-10 15:58:44 +07:00
parent 3efdb52c3c
commit e5191ebe50
9 changed files with 204 additions and 13 deletions
+5
View File
@@ -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
```
+122
View File
@@ -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>')
+8
View File
@@ -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