#!/usr/bin/env bash set -euo pipefail HOST="169.254.100.2" PORT="48888" PASSWORD="2026" INTERFACE="" usage() { cat <<'EOF' Usage: linux-negative-test.sh [options] Purpose: Safe negative checks for the Linux server. This script does not call /api/network/apply and does not modify netplan. Checks: - Wrong password should be rejected - Missing interface should fail validate - Invalid IP should fail validate - Gateway outside subnet should fail validate - Invalid DNS should fail validate Options: --host Server host, default: 169.254.100.2 --port Server port, default: 48888 --password Correct admin password, default: 2026 --interface Existing target interface, default: suggested_target_interface --help Show this help EOF } while [[ $# -gt 0 ]]; do case "$1" in --host) HOST="$2" shift 2 ;; --port) PORT="$2" shift 2 ;; --password) PASSWORD="$2" shift 2 ;; --interface) INTERFACE="$2" shift 2 ;; --help|-h) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done for cmd in curl python3; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "Missing required command: $cmd" >&2 exit 2 fi done BASE_URL="http://${HOST}:${PORT}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT log() { printf '[nettool-negative] %s\n' "$*" } request() { local method="$1" local path="$2" local password="$3" local payload="${4:-}" local body_file="$TMP_DIR/body.json" local http_code if [[ -n "$payload" ]]; then http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \ -H "X-Admin-Password: ${password}" \ -H "Content-Type: application/json" \ --data "$payload" \ --connect-timeout 3 \ --max-time 15 \ "${BASE_URL}${path}")" else http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \ -H "X-Admin-Password: ${password}" \ --connect-timeout 3 \ --max-time 15 \ "${BASE_URL}${path}")" fi printf '%s\n' "$http_code" cat "$body_file" } read_http_code() { sed -n '1p' } read_body() { sed '1d' } json_get() { local body="$1" local expr="$2" BODY_JSON="$body" python3 - "$expr" <<'PY' import json import os import sys expr = sys.argv[1] data = json.loads(os.environ["BODY_JSON"]) value = data for part in expr.split('.'): if part: value = value[part] if isinstance(value, list): import json as _json print(_json.dumps(value, ensure_ascii=False)) else: print(value) PY } expect_failure() { local name="$1" local expected_http="$2" local expected_code="$3" local result="$4" local http_code body actual_code http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" actual_code="$(json_get "$body" "code")" if [[ "$http_code" != "$expected_http" ]]; then echo "${name} failed: expected http=${expected_http}, actual=${http_code}, body=${body}" >&2 exit 1 fi if [[ "$actual_code" != "$expected_code" ]]; then echo "${name} failed: expected code=${expected_code}, actual=${actual_code}, body=${body}" >&2 exit 1 fi log "${name} -> expected failure confirmed (http=${http_code}, code=${actual_code})" } build_payload() { python3 - "$1" "$2" "$3" "$4" "$5" <<'PY' import json import sys interface, ip, prefix, gateway, dns_csv = sys.argv[1:6] dns = [item.strip() for item in dns_csv.split(',') if item.strip()] payload = { "interface": interface, "ip": ip, "prefix": int(prefix), "gateway": gateway, "dns": dns, } print(json.dumps(payload, ensure_ascii=False)) PY } log "discover interface" result="$(request GET "/api/network/interfaces" "$PASSWORD")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" if [[ "$http_code" != "200" ]]; then echo "Failed to query interfaces: http=${http_code} body=${body}" >&2 exit 1 fi if [[ -z "$INTERFACE" ]]; then INTERFACE="$(json_get "$body" "data.suggested_target_interface")" fi if [[ -z "$INTERFACE" || "$INTERFACE" == "None" ]]; then echo "Could not determine target interface. Pass --interface explicitly." >&2 exit 1 fi log "target interface=${INTERFACE}" log "wrong password should be rejected" result="$(request GET "/api/health" "wrong-password")" expect_failure "wrong password health" "401" "1002" "$result" log "missing interface should fail validate" payload="$(build_payload "not-a-real-iface" "192.168.10.20" "24" "192.168.10.1" "8.8.8.8")" result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")" expect_failure "invalid interface validate" "400" "3001" "$result" log "invalid ip should fail validate" payload="$(build_payload "$INTERFACE" "999.999.1.1" "24" "192.168.10.1" "8.8.8.8")" result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")" expect_failure "invalid ip validate" "400" "3001" "$result" log "gateway outside subnet should fail validate" payload="$(build_payload "$INTERFACE" "192.168.10.20" "24" "10.0.0.1" "8.8.8.8")" result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")" expect_failure "gateway subnet validate" "400" "3001" "$result" log "invalid dns should fail validate" payload="$(build_payload "$INTERFACE" "192.168.10.20" "24" "192.168.10.1" "8.8.8.8,not-an-ip")" result="$(request POST "/api/network/validate" "$PASSWORD" "$payload")" expect_failure "invalid dns validate" "400" "3001" "$result" log "negative checks completed"