156 lines
9.2 KiB
Bash
156 lines
9.2 KiB
Bash
#!/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"
|
||
FLOW_TEMPLATE_LIFETIME_MS: \${FLOW_TEMPLATE_LIFETIME_MS:-5400000}
|
||
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 обновите статус хоста и нажмите «Применить настройки хоста»."
|