Files

155 lines
9.2 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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" <<EOF
services:
netflow-agent:
image: "$AGENT_IMAGE"
container_name: mmapp-netflow-agent
restart: unless-stopped
network_mode: host
cap_drop: [ALL]
cap_add: [NET_ADMIN]
security_opt: ["no-new-privileges:true"]
read_only: true
tmpfs: ["/tmp:rw,noexec,nosuid,size=8m"]
volumes:
- ./data/netflow-wireguard:/etc/wireguard
- ./data/netflow-agent-socket:/run/mm-netflow-host
logging:
driver: json-file
options: {max-size: "5m", max-file: "2"}
backend:
environment:
FLOW_AGENT_SOCKET: /run/mm-netflow-host/agent.sock
FLOW_LISTEN_HOST: "0.0.0.0"
volumes:
- ./data/netflow-agent-socket:/run/mm-netflow-host:ro
ports:
- "$COLLECTOR_IP:$FLOW_PORT:$FLOW_PORT/udp"
depends_on:
netflow-agent:
condition: service_healthy
EOF
COMPOSE_ARGS=(-f "$COMPOSE_DIR/docker-compose.yml")
if [[ -f "$COMPOSE_DIR/docker-compose.override.yml" ]]; then COMPOSE_ARGS+=(-f "$COMPOSE_DIR/docker-compose.override.yml"); fi
# Render first, and reject an existing public/incorrect IPFIX binding before touching WireGuard.
docker compose --project-directory "$COMPOSE_DIR" "${COMPOSE_ARGS[@]}" -f "$TASK_TEMP/compose.yml" config --format json > "$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 обновите статус хоста и нажмите «Применить настройки хоста»."