#!/usr/bin/env bash # Usage: sudo bash install.sh /absolute/path/wg-flow.conf /opt/cdn-mm 4739 set -euo pipefail umask 077 [[ ${EUID:-$(id -u)} -eq 0 ]] || { echo "Запустите от root" >&2; exit 1; } SOURCE=$(realpath "${1:?Укажите скачанный wg-flow.conf}") COMPOSE_DIR=$(realpath "${2:-/opt/cdn-mm}") FLOW_PORT=${3:-4739} [[ "$FLOW_PORT" =~ ^[0-9]+$ ]] && (( FLOW_PORT >= 1 && FLOW_PORT <= 65535 )) || exit 1 command -v docker >/dev/null docker compose version >/dev/null systemctl show docker.service --property=LoadState --value | grep -qx loaded if ! command -v wg >/dev/null || ! command -v python3 >/dev/null || ! command -v modprobe >/dev/null; then apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard-tools python3 iproute2 kmod fi modprobe wireguard [[ -f "$COMPOSE_DIR/docker-compose.yml" ]] || { echo "Не найден docker-compose.yml" >&2; exit 1; } AGENT_IMAGE=${MM_NETFLOW_AGENT_IMAGE:-git.shx.one/denozord/mikrotikmanager-netflow-agent:latest} [[ "$AGENT_IMAGE" =~ ^[A-Za-z0-9][A-Za-z0-9._:/@-]+$ ]] || { echo "Некорректное имя образа" >&2; exit 1; } # The image must be available before changing the host configuration. docker pull "$AGENT_IMAGE" TASK_TEMP=$(mktemp -d) trap 'rm -rf -- "$TASK_TEMP"' EXIT python3 - "$SOURCE" "$COMPOSE_DIR" "$FLOW_PORT" "$TASK_TEMP" <<'PY' import base64, ipaddress, json, os, pathlib, re, subprocess, sys source, directory, port, temp = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), int(sys.argv[3]), pathlib.Path(sys.argv[4]) def read(text): section, fields, peers = None, {}, [] allowed = {'Interface': {'PrivateKey','Address','ListenPort','MTU'}, 'Peer': {'PublicKey','AllowedIPs','Endpoint','PersistentKeepalive','PresharedKey'}} for raw in text.splitlines(): line = raw.split('#',1)[0].strip() if not line: continue if line == '[Interface]' and section is None: section, current = 'Interface', fields elif line == '[Peer]' and section is not None: section, current = 'Peer', {}; peers.append(current) else: key, sep, value = line.partition('='); key, value = key.strip(), value.strip() if not sep or section not in allowed or key not in allowed[section] or key in current or not value: raise SystemExit('Конфигурация содержит неподдерживаемые параметры; требуется ручная проверка') current[key] = value try: addr = ipaddress.IPv4Interface(fields['Address']) if addr.network.prefixlen != 24 or addr.ip in (addr.network.network_address, addr.network.broadcast_address): raise ValueError() if len(base64.b64decode(fields['PrivateKey'], validate=True)) != 32 or not 1 <= int(fields['ListenPort']) <= 65535: raise ValueError() if 'MTU' in fields and not 576 <= int(fields['MTU']) <= 9000: raise ValueError() for peer in peers: if len(base64.b64decode(peer['PublicKey'], validate=True)) != 32: raise ValueError() for network in peer['AllowedIPs'].split(','): ipaddress.ip_network(network.strip(), strict=False) if 'Endpoint' in peer and not re.fullmatch(r'(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):[0-9]{1,5}', peer['Endpoint']): raise ValueError() if 'PersistentKeepalive' in peer and not 0 <= int(peer['PersistentKeepalive']) <= 65535: raise ValueError() except (ValueError, KeyError): raise SystemExit('Некорректная конфигурация WireGuard; ключи не изменены') return fields, addr expected, addr = read(source.read_text()) active = pathlib.Path('/etc/wireguard/wg-flow.conf') candidate = active.read_text() if active.exists() else source.read_text() actual, actual_addr = read(candidate) for key in ('PrivateKey','Address','ListenPort'): if actual[key] != expected[key]: raise SystemExit('Ключ, адрес или порт существующего туннеля отличаются от MM; установка остановлена') public = subprocess.run(['wg','pubkey'], input=actual['PrivateKey']+'\n',text=True,capture_output=True,check=True).stdout.strip() if subprocess.run(['ip','link','show','dev','wg-flow'],capture_output=True).returncode == 0: dump = subprocess.run(['wg','show','wg-flow','dump'],capture_output=True,text=True,check=True).stdout.splitlines() head = dump[0].split('\t') addresses = json.loads(subprocess.run(['ip','-j','-4','address','show','dev','wg-flow'],capture_output=True,text=True,check=True).stdout) if head[1] != public or int(head[2]) != int(actual['ListenPort']) or not any(a.get('local') == str(addr.ip) and a.get('prefixlen') == addr.network.prefixlen for row in addresses for a in row.get('addr_info',[])): raise SystemExit('Работающий wg-flow отличается от MM; установка остановлена') binding = dict(collectorIp=str(addr.ip), prefix=str(addr.network), flowListenPort=port, wgListenPort=int(actual['ListenPort'])) (temp/'binding.json').write_text(json.dumps(binding)) (temp/'wg-flow.conf').write_text(candidate) (temp/'collector').write_text(str(addr.ip)) PY COLLECTOR_IP=$(cat "$TASK_TEMP/collector") # Keep a dedicated override; never replace docker-compose.override.yml. cat > "$TASK_TEMP/compose.yml" < "$TASK_TEMP/merged.json" python3 - "$TASK_TEMP/merged.json" "$COLLECTOR_IP" "$FLOW_PORT" <<'PY' import json,sys config=json.load(open(sys.argv[1])); ip=sys.argv[2]; port=int(sys.argv[3]) for bind in config['services']['backend'].get('ports',[]): if bind.get('protocol','tcp') == 'udp' and (bind['target'] in (4739,port) or str(bind.get('published')) == str(port)): if bind.get('host_ip') != ip or bind['target'] != port or str(bind.get('published')) != str(port): raise SystemExit('В Compose уже есть другой IPFIX bind. Согласуйте его с адресом и портом MM и повторите установку.') PY if docker container inspect mmapp-netflow-agent >/dev/null 2>&1; then docker stop mmapp-netflow-agent; fi install -d -m 700 /etc/wireguard "$COMPOSE_DIR/data/netflow-wireguard" "$COMPOSE_DIR/data/netflow-agent-socket" if [[ -e /etc/wireguard/wg-flow.conf ]]; then cp -L /etc/wireguard/wg-flow.conf "$COMPOSE_DIR/data/netflow-wireguard/wg-flow.conf.backup.$(date +%s)" fi install -m 600 "$TASK_TEMP/wg-flow.conf" "$COMPOSE_DIR/data/netflow-wireguard/wg-flow.conf" install -m 600 "$TASK_TEMP/binding.json" "$COMPOSE_DIR/data/netflow-wireguard/binding.json" ln -sfn "$COMPOSE_DIR/data/netflow-wireguard/wg-flow.conf" /etc/wireguard/wg-flow.conf install -m 600 "$TASK_TEMP/compose.yml" "$COMPOSE_DIR/docker-compose.netflow-agent.yml" install -d /etc/systemd/system/docker.service.d cat > /etc/systemd/system/docker.service.d/mm-netflow.conf <<'EOF' [Unit] Wants=wg-quick@wg-flow.service After=wg-quick@wg-flow.service EOF systemctl daemon-reload systemctl enable wg-quick@wg-flow.service if ! ip link show dev wg-flow >/dev/null 2>&1; then systemctl start wg-quick@wg-flow.service else ip link set dev wg-flow up fi cd "$COMPOSE_DIR" docker compose "${COMPOSE_ARGS[@]}" -f docker-compose.netflow-agent.yml up -d netflow-agent AGENT_READY=false for attempt in $(seq 1 60); do if [[ "$(docker inspect --format '{{.State.Health.Status}}' mmapp-netflow-agent)" == healthy ]]; then AGENT_READY=true break fi sleep 1 done [[ "$AGENT_READY" == true ]] || { echo "Агент не готов. Проверьте docker logs mmapp-netflow-agent; backend не пересоздан." >&2; exit 1; } docker compose "${COMPOSE_ARGS[@]}" -f docker-compose.netflow-agent.yml up -d --no-deps --force-recreate backend echo "Агент установлен. Используйте те же -f файлы при дальнейших операциях Compose:" printf 'docker compose '; printf '%q ' "${COMPOSE_ARGS[@]}" -f "$COMPOSE_DIR/docker-compose.netflow-agent.yml"; echo 'up -d' echo "В MM обновите статус хоста и нажмите «Применить настройки хоста»."