1
0
mirror of https://github.com/shadowsocks/shadowsocks-libev.git synced 2026-09-24 00:45:06 +00:00
Files
shadowsocks-libev/tests/docker_smoke.py

127 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Exercise the scratch server/client images with real TCP and UDP traffic."""
import argparse
import base64
import concurrent.futures
import json
import os
import socket
import time
from pathlib import Path
import subprocess
import tempfile
import threading
import uuid
import interop
def inside():
# All three containers share an isolated network namespace. Only loopback
# is needed, including for the server's localhost hostname resolution.
for port in (8388, 1080):
deadline = time.monotonic() + 15
while True:
try:
with socket.create_connection(('127.0.0.1', port), timeout=.2):
break
except OSError:
if time.monotonic() >= deadline:
raise AssertionError(f'container failed to listen on {port}')
time.sleep(.05)
with interop.TCPOrigin(('127.0.0.1', 0), interop.TCPHandler) as tcp, \
interop.UDPOrigin(('127.0.0.1', 0), interop.UDPHandler) as udp:
for origin in (tcp, udp):
threading.Thread(target=origin.serve_forever, daemon=True).start()
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(interop.tcp_case, 1080, tcp.server_address[1], size)
for size in (1, 65536, 1048576)]
for future in futures:
future.result(timeout=30)
interop.udp_case(1080, udp.server_address[1])
finally:
tcp.shutdown()
udp.shutdown()
print('PASS scratch image: concurrent TCP, hostname resolution, and UDP', flush=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--image', default='shadowsocks-c:test')
parser.add_argument('--python-image', default='python:3.12-alpine')
parser.add_argument('--inside', action='store_true', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.inside:
inside()
return
root = Path(__file__).resolve().parent
name = 'ss-docker-test-' + uuid.uuid4().hex[:12]
helper = name + '-helper'
with tempfile.TemporaryDirectory(prefix='ss-docker-test-') as directory:
context = Path(directory)
for filename in ('interop.py', 'docker_smoke.py'):
(context / filename).write_bytes((root / filename).read_bytes())
(context / 'Dockerfile').write_text(
'ARG PYTHON_IMAGE=python:3.12-alpine\nFROM ${PYTHON_IMAGE}\n'
'COPY interop.py docker_smoke.py /tests/\n'
'USER 65532:65532\nENTRYPOINT ["python3", "/tests/docker_smoke.py", "--inside"]\n')
subprocess.run(['docker', 'build', '--build-arg', 'PYTHON_IMAGE=' + args.python_image,
'-t', helper, str(context)], check=True)
try:
for method in ('aes-256-gcm', '2022-blake3-aes-128-gcm'):
containers = []
volume = name + '-config'
key = base64.b64encode(os.urandom(16)).decode('ascii')
try:
common = ['docker', 'run', '-d', '--read-only', '--cap-drop=ALL',
'--security-opt=no-new-privileges:true']
server = name + '-server'
client = name + '-client'
subprocess.run(['docker', 'volume', 'create', volume], check=True,
stdout=subprocess.DEVNULL)
config = json.dumps({'server': '0.0.0.0', 'server_port': 8388,
'password': key, 'method': method, 'mode': 'tcp_and_udp'})
subprocess.run(['docker', 'run', '--rm', '-i', '--network', 'none',
'--user', '0:0', '--entrypoint', 'python3',
'--mount', 'type=volume,src=' + volume + ',dst=/config', helper,
'-c', 'import pathlib,sys; p=pathlib.Path("/config/config.json"); '
'p.write_text(sys.stdin.read()); p.chmod(0o644)'],
input=config, text=True, check=True)
subprocess.run(common + ['--name', server, '--network', 'none',
'--mount', 'type=volume,src=' + volume +
',dst=/etc/shadowsocks-c,readonly', args.image], check=True)
containers.append(server)
subprocess.run(common + ['--name', client, '--network', 'container:' + server,
'--entrypoint', '/usr/local/bin/ss-local', args.image,
'-s', '127.0.0.1', '-p', '8388', '-l', '1080', '-k', key,
'-m', method, '-u'], check=True)
containers.append(client)
# The helper waits for both listeners before generating traffic.
subprocess.run(['docker', 'run', '--rm', '--network', 'container:' + server,
'--read-only', '--cap-drop=ALL', helper], check=True, timeout=90)
for container in containers:
assert subprocess.check_output(['docker', 'inspect', '--format',
'{{.State.Running}}', container], text=True).strip() == 'true'
for container in reversed(containers):
subprocess.run(['docker', 'stop', '--time', '5', container], check=True,
stdout=subprocess.DEVNULL)
code = subprocess.check_output(['docker', 'inspect', '--format',
'{{.State.ExitCode}}', container], text=True).strip()
assert code == '0', f'{container}: unclean shutdown ({code})'
print('PASS runtime image: ' + method, flush=True)
except BaseException:
for container in containers:
subprocess.run(['docker', 'logs', container], check=False)
raise
finally:
for container in reversed(containers):
subprocess.run(['docker', 'rm', '-f', container], check=False, stdout=subprocess.DEVNULL)
subprocess.run(['docker', 'volume', 'rm', volume], check=False, stdout=subprocess.DEVNULL)
finally:
subprocess.run(['docker', 'image', 'rm', helper], check=False, stdout=subprocess.DEVNULL)
if __name__ == '__main__':
main()