#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" TEST_NAME="ddospot" DEFAULT_IMAGE="dtagdevsec/ddospot:24.04.1" IMAGE="" CHARGEN_PORT="" DNS_PORT="" NTP_PORT="" SSDP_PORT="" LOG_DIR="" BL_DIR="" DB_DIR="" CHARGEN_LOG_FILE="" DNS_LOG_FILE="" NTP_LOG_FILE="" SSDP_LOG_FILE="" MAPPED_CHARGEN_PORT="" MAPPED_DNS_PORT="" MAPPED_NTP_PORT="" MAPPED_SSDP_PORT="" usage() { cat <." fi } prepare_ddospot_harness() { test_prepare_harness "${TEST_NAME}" LOG_DIR="${TEST_TMP_ROOT}/log" BL_DIR="${TEST_TMP_ROOT}/bl" DB_DIR="${TEST_TMP_ROOT}/db" CHARGEN_LOG_FILE="${LOG_DIR}/chargenpot.log" DNS_LOG_FILE="${LOG_DIR}/dnspot.log" NTP_LOG_FILE="${LOG_DIR}/ntpot.log" SSDP_LOG_FILE="${LOG_DIR}/ssdpot.log" TEST_ARTIFACT_LOG_DIR="${LOG_DIR}" mkdir -p "${LOG_DIR}" "${BL_DIR}" "${DB_DIR}" chmod 0777 "${LOG_DIR}" "${BL_DIR}" "${DB_DIR}" cat > "${TEST_HARNESS_COMPOSE}" <> 3) & 0x07 if mode != 4: raise RuntimeError(f"Expected NTP server mode 4, got mode {mode}") if version < 1: raise RuntimeError(f"Expected sane NTP version, got {version}") print(f"NTP client-mode probe received {len(response)} bytes") except Exception as exc: print(f"NTP probe failed: {exc}", file=sys.stderr) sys.exit(1) PY } run_ssdp_probe() { local token="$1" python3 - "${TEST_BIND_IP}" "${MAPPED_SSDP_PORT}" "${token}" "${TEST_TIMEOUT}" <<'PY' import socket import sys host = sys.argv[1] port = int(sys.argv[2]) token = sys.argv[3] timeout = int(sys.argv[4]) request = ( "M-SEARCH * HTTP/1.1\r\n" "HOST: 239.255.255.250:1900\r\n" "MAN: \"ssdp:discover\"\r\n" "MX: 1\r\n" "ST: ssdp:all\r\n" f"USER-AGENT: tpot-ddospot-smoke/{token}\r\n" "\r\n" ).encode("ascii") try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.settimeout(min(timeout, 3)) sock.sendto(request, (host, port)) try: response, _ = sock.recvfrom(4096) except socket.timeout: response = b"" if not response: raise RuntimeError("Expected SSDP response, got no UDP response") upper = response.upper() if not (upper.startswith(b"HTTP/") or b"SSDP" in upper or b"UPNP" in upper): raise RuntimeError(f"Expected SSDP-like HTTP response, got {response[:120]!r}") status = response.splitlines()[0].decode("iso-8859-1", errors="replace") if response.splitlines() else "" print(f"SSDP probe response: {status}") except Exception as exc: print(f"SSDP probe failed: {exc}", file=sys.stderr) sys.exit(1) PY } run_probe_with_retries() { local description="$1" shift local deadline=$((SECONDS + TEST_TIMEOUT)) local output="" while (( SECONDS < deadline )); do if output="$("$@" 2>&1)"; then printf '%s\n' "${output}" return 0 fi sleep 1 done printf '%s\n' "${output}" >&2 printf '%s probe did not succeed before timeout\n' "${description}" >&2 return 1 } wait_for_json_event() { local json_file="$1" local service="$2" python3 - "${json_file}" "${service}" "${TEST_TIMEOUT}" <<'PY' import json import sys import time from pathlib import Path path = Path(sys.argv[1]) service = sys.argv[2] timeout = int(sys.argv[3]) deadline = time.monotonic() + timeout last_error = None while time.monotonic() < deadline: if not path.exists(): last_error = f"{path} does not exist yet" time.sleep(1) continue raw = path.read_text(encoding="utf-8", errors="replace") if not raw.strip(): last_error = f"{path} is empty" time.sleep(1) continue invalid = None for line_number, line in enumerate(raw.splitlines(), 1): if not line.strip(): continue try: event = json.loads(line) except json.JSONDecodeError as exc: invalid = f"{path}:{line_number}: invalid JSON: {exc}" continue if isinstance(event, dict): print(f"{service} JSON event found in {path}:{line_number}") sys.exit(0) invalid = f"{path}:{line_number}: JSON event is not an object" last_error = invalid or f"No JSON event found in {path}" time.sleep(1) if last_error: print(last_error, file=sys.stderr) sys.exit(1) PY } assert_no_runtime_errors() { if grep -R -E "Traceback|NameError|Exception" "${LOG_DIR}" >/dev/null 2>&1; then test_die "DDoSPot runtime error found in log files" fi if test_compose logs --no-color 2>/dev/null | grep -E "Traceback|NameError|Exception" >/dev/null 2>&1; then test_die "DDoSPot runtime error found in Docker logs" fi } main() { parse_args "$@" validate_args test_check_dependencies if [[ -z "${IMAGE}" ]]; then IMAGE="$(test_read_compose_image "${TEST_NAME}" "${DEFAULT_IMAGE}")" fi test_info "Using image: ${IMAGE}" test_require_image "${IMAGE}" "docker compose -f docker/${TEST_NAME}/docker-compose.yml build ${TEST_NAME}" ensure_udp_port_free_for_docker "${CHARGEN_PORT}" "--chargen-port" ensure_udp_port_free_for_docker "${DNS_PORT}" "--dns-port" ensure_udp_port_free_for_docker "${NTP_PORT}" "--ntp-port" ensure_udp_port_free_for_docker "${SSDP_PORT}" "--ssdp-port" prepare_ddospot_harness test_enable_cleanup test_info "Starting isolated DDoSPot container" test_compose up -d --no-build >/dev/null test_wait_for_container || test_die "DDoSPot container did not stay running" test_ok "Container is running" MAPPED_CHARGEN_PORT="$(test_get_mapped_port "${TEST_NAME}" "19/udp")" || test_die "Could not resolve mapped host port for 19/udp" test_ok "Port ${TEST_BIND_IP}:${MAPPED_CHARGEN_PORT} maps to container port 19/udp" MAPPED_DNS_PORT="$(test_get_mapped_port "${TEST_NAME}" "53/udp")" || test_die "Could not resolve mapped host port for 53/udp" test_ok "Port ${TEST_BIND_IP}:${MAPPED_DNS_PORT} maps to container port 53/udp" MAPPED_NTP_PORT="$(test_get_mapped_port "${TEST_NAME}" "123/udp")" || test_die "Could not resolve mapped host port for 123/udp" test_ok "Port ${TEST_BIND_IP}:${MAPPED_NTP_PORT} maps to container port 123/udp" MAPPED_SSDP_PORT="$(test_get_mapped_port "${TEST_NAME}" "1900/udp")" || test_die "Could not resolve mapped host port for 1900/udp" test_ok "Port ${TEST_BIND_IP}:${MAPPED_SSDP_PORT} maps to container port 1900/udp" local token="ddospot-test-$(date +%s)-$$" test_info "Running CHARGEN probe with token: ${token}" run_probe_with_retries "CHARGEN" run_chargen_probe "${token}" || test_die "CHARGEN probe failed on ${TEST_BIND_IP}:${MAPPED_CHARGEN_PORT}" test_wait_for_container || test_die "DDoSPot container stopped after CHARGEN probe" test_info "Running DNS CHAOS/TXT probe with token: ${token}" run_probe_with_retries "DNS" run_dns_probe "${token}" || test_die "DNS probe failed on ${TEST_BIND_IP}:${MAPPED_DNS_PORT}" test_wait_for_container || test_die "DDoSPot container stopped after DNS probe" test_info "Running NTP client-mode probe" run_probe_with_retries "NTP" run_ntp_probe || test_die "NTP probe failed on ${TEST_BIND_IP}:${MAPPED_NTP_PORT}" test_wait_for_container || test_die "DDoSPot container stopped after NTP probe" test_info "Running SSDP M-SEARCH probe with token: ${token}" run_probe_with_retries "SSDP" run_ssdp_probe "${token}" || test_die "SSDP probe failed on ${TEST_BIND_IP}:${MAPPED_SSDP_PORT}" test_wait_for_container || test_die "DDoSPot container stopped after SSDP probe" test_info "Waiting for DDoSPot JSON log events" wait_for_json_event "${CHARGEN_LOG_FILE}" "CHARGEN" || test_die "CHARGEN JSON event was not found in chargenpot.log" wait_for_json_event "${DNS_LOG_FILE}" "DNS" || test_die "DNS JSON event was not found in dnspot.log" wait_for_json_event "${NTP_LOG_FILE}" "NTP" || test_die "NTP JSON event was not found in ntpot.log" wait_for_json_event "${SSDP_LOG_FILE}" "SSDP" || test_die "SSDP JSON event was not found in ssdpot.log" test_ok "All DDoSPot JSON events were written" assert_no_runtime_errors test_ok "No DDoSPot runtime errors found in logs" test_ok "DDoSPot post-build smoke test completed successfully" } main "$@"