2026-05-15 16:47:16 +03:00
|
|
|
|
#!/bin/bash
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# Montana Node — авто-присоединение к сети.
|
2026-05-15 16:47:16 +03:00
|
|
|
|
#
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# Два режима:
|
2026-05-15 16:57:34 +03:00
|
|
|
|
#
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 1. Permissionless P2P-only (любой может) — узел в сети Montana, на карте /net/,
|
|
|
|
|
|
# НО НЕ в cascade VPN balancer:
|
|
|
|
|
|
# ALIAS=cologne LABEL=Köln COUNTRY=DE HOSTING=Hetzner \
|
|
|
|
|
|
# COORDS="50.94,6.96" bash join.sh
|
2026-05-15 16:57:34 +03:00
|
|
|
|
#
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 2. Permissioned VPN-backend (нужен TOKEN от admin) — узел дополнительно
|
|
|
|
|
|
# попадает в Helsinki cascade VPN balancer:
|
|
|
|
|
|
# ROLE=vpn-backend TOKEN=<orch_token> \
|
|
|
|
|
|
# ALIAS=cologne LABEL=Köln COUNTRY=DE HOSTING=Hetzner \
|
|
|
|
|
|
# COORDS="50.94,6.96" bash join.sh
|
2026-05-15 16:47:16 +03:00
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
: "${ALIAS:?нужен ALIAS, например: amsterdam}"
|
|
|
|
|
|
: "${LABEL:?нужен LABEL, например: Amsterdam}"
|
|
|
|
|
|
: "${COUNTRY:?нужен COUNTRY 2-буквы, например: NL}"
|
|
|
|
|
|
: "${HOSTING:?нужен HOSTING, например: DigitalOcean}"
|
|
|
|
|
|
: "${COORDS:?нужны COORDS \"lat,lon\", например: 52.37,4.89}"
|
|
|
|
|
|
ROLE="${ROLE:-node-only}" # node-only | vpn-backend
|
2026-05-15 16:47:16 +03:00
|
|
|
|
ORCH="${ORCH:-https://montana.quest/vpn/node}"
|
2026-05-15 16:57:34 +03:00
|
|
|
|
HUB="${HUB:-https://hub.montana.quest/efir369999/montana/raw/branch/main}"
|
2026-05-18 18:05:32 +03:00
|
|
|
|
BOOTSTRAP_FROM="${BOOTSTRAP_FROM:-cdn.montana.quest}"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$ROLE" = "vpn-backend" ]; then
|
|
|
|
|
|
: "${TOKEN:?для vpn-backend нужен TOKEN от admin}"
|
|
|
|
|
|
fi
|
2026-05-15 16:57:34 +03:00
|
|
|
|
|
2026-05-15 16:47:16 +03:00
|
|
|
|
PUBLIC_IP="$(curl -s https://api.ipify.org)"
|
2026-05-18 18:05:32 +03:00
|
|
|
|
echo "==> Montana Node join: $ALIAS ($LABEL, $COUNTRY/$HOSTING) role=$ROLE ip=$PUBLIC_IP"
|
2026-05-15 16:47:16 +03:00
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 1. Зависимости
|
2026-05-15 16:47:16 +03:00
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
|
|
apt-get update -qq
|
2026-05-18 22:11:45 +03:00
|
|
|
|
apt-get install -y -qq curl ca-certificates openssl unzip jq fail2ban
|
2026-05-15 16:47:16 +03:00
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 2. ufw
|
2026-05-15 16:47:16 +03:00
|
|
|
|
ufw default deny incoming >/dev/null 2>&1 || true
|
|
|
|
|
|
ufw default allow outgoing >/dev/null 2>&1 || true
|
|
|
|
|
|
ufw allow 22/tcp >/dev/null
|
2026-05-18 18:05:32 +03:00
|
|
|
|
ufw allow 8444/tcp >/dev/null # p2p Montana — всегда открыт
|
|
|
|
|
|
[ "$ROLE" = "vpn-backend" ] && ufw allow 443/tcp >/dev/null
|
2026-05-15 16:47:16 +03:00
|
|
|
|
echo "y" | ufw enable >/dev/null
|
|
|
|
|
|
|
2026-05-18 22:11:45 +03:00
|
|
|
|
# fail2ban: ssh brute-force защита (memory project_fail2ban)
|
|
|
|
|
|
cat > /etc/fail2ban/jail.d/montana.conf <<F2B
|
|
|
|
|
|
[sshd]
|
|
|
|
|
|
enabled = true
|
|
|
|
|
|
maxretry = 3
|
|
|
|
|
|
findtime = 10m
|
|
|
|
|
|
bantime = 1h
|
|
|
|
|
|
bantime.increment = true
|
|
|
|
|
|
bantime.maxtime = 1w
|
|
|
|
|
|
backend = auto
|
|
|
|
|
|
F2B
|
|
|
|
|
|
systemctl enable --now fail2ban
|
|
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 3. montana-node — скачать бинарь с хаба, manifest с хаба
|
2026-05-15 16:57:34 +03:00
|
|
|
|
mkdir -p /etc/montana
|
2026-05-18 18:05:32 +03:00
|
|
|
|
if [ ! -x /usr/local/bin/montana-node ]; then
|
|
|
|
|
|
echo "==> Скачиваю montana-node бинарь с $HUB/Node/bin/montana-node"
|
|
|
|
|
|
curl -sL "$HUB/Node/bin/montana-node" -o /usr/local/bin/montana-node
|
|
|
|
|
|
chmod +x /usr/local/bin/montana-node
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ ! -f /etc/montana/genesis-manifest.json ]; then
|
|
|
|
|
|
curl -sL "$HUB/Node/genesis-manifest.json" -o /etc/montana/genesis-manifest.json
|
|
|
|
|
|
fi
|
2026-05-15 16:47:16 +03:00
|
|
|
|
|
2026-05-15 16:57:34 +03:00
|
|
|
|
id montana &>/dev/null || useradd -r -m -d /var/lib/montana -s /usr/sbin/nologin montana
|
|
|
|
|
|
mkdir -p /var/lib/montana
|
|
|
|
|
|
chown -R montana:montana /var/lib/montana && chmod 700 /var/lib/montana
|
2026-05-18 18:05:32 +03:00
|
|
|
|
|
2026-05-15 16:57:34 +03:00
|
|
|
|
[ -f /var/lib/montana/identity.bin ] || sudo -u montana /usr/local/bin/montana-node init --data-dir /var/lib/montana
|
2026-05-15 16:47:16 +03:00
|
|
|
|
|
|
|
|
|
|
cat > /etc/systemd/system/montana-node.service <<UNIT
|
|
|
|
|
|
[Unit]
|
2026-05-18 18:05:32 +03:00
|
|
|
|
Description=Montana Local Node (M8 cross-machine, Proof-of-Time)
|
2026-05-15 16:47:16 +03:00
|
|
|
|
After=network.target
|
|
|
|
|
|
Wants=network-online.target
|
|
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
|
Type=simple
|
|
|
|
|
|
User=montana
|
|
|
|
|
|
Group=montana
|
|
|
|
|
|
ExecStart=/usr/local/bin/montana-node start --data-dir /var/lib/montana --listen /ip4/0.0.0.0/tcp/8444 --genesis-manifest /etc/montana/genesis-manifest.json
|
|
|
|
|
|
Restart=on-failure
|
|
|
|
|
|
RestartSec=10
|
|
|
|
|
|
NoNewPrivileges=yes
|
|
|
|
|
|
PrivateTmp=yes
|
|
|
|
|
|
ProtectSystem=strict
|
|
|
|
|
|
ProtectHome=yes
|
|
|
|
|
|
ReadWritePaths=/var/lib/montana
|
|
|
|
|
|
CPUQuota=200%
|
|
|
|
|
|
LimitNOFILE=8192
|
|
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
|
UNIT
|
|
|
|
|
|
systemctl daemon-reload
|
|
|
|
|
|
systemctl enable --now montana-node
|
2026-05-18 18:05:32 +03:00
|
|
|
|
sleep 5
|
|
|
|
|
|
systemctl is-active montana-node && echo "montana-node: active" || echo "!! montana-node fail"
|
|
|
|
|
|
|
2026-05-18 22:11:45 +03:00
|
|
|
|
# 4. xray (только для vpn-backend role) — УНИВЕРСАЛЬНЫЙ ключ на все узлы.
|
|
|
|
|
|
# Публичные параметры зашиты в скрипте (UUID/PBK/SID — попадают в подписку клиента).
|
|
|
|
|
|
# privateKey — секрет, читается из env VPN_PRIVKEY либо из /etc/montana/vpn-privkey
|
|
|
|
|
|
# (admin кладёт по защищённому каналу). НЕ хранить в этом файле.
|
|
|
|
|
|
UUID="e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d"
|
|
|
|
|
|
PBK="EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8"
|
|
|
|
|
|
SID="302805bc0c25e504"
|
|
|
|
|
|
SNI="www.googletagmanager.com"
|
|
|
|
|
|
XRAY_PIN="v25.10.21" # фиксируем версию xray-install
|
2026-05-18 18:05:32 +03:00
|
|
|
|
if [ "$ROLE" = "vpn-backend" ]; then
|
2026-05-18 22:11:45 +03:00
|
|
|
|
if [ -z "${VPN_PRIVKEY:-}" ] && [ -f /etc/montana/vpn-privkey ]; then
|
|
|
|
|
|
VPN_PRIVKEY="$(cat /etc/montana/vpn-privkey)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
: "${VPN_PRIVKEY:?нужен VPN_PRIVKEY (Reality privateKey, у admin) для ROLE=vpn-backend}"
|
|
|
|
|
|
PRIV="$VPN_PRIVKEY"
|
|
|
|
|
|
mkdir -p /etc/montana && chmod 700 /etc/montana
|
|
|
|
|
|
install -m 0600 /dev/stdin /etc/montana/vpn-privkey <<<"$PRIV"
|
|
|
|
|
|
curl -fsSL "https://github.com/XTLS/Xray-install/raw/${XRAY_PIN}/install-release.sh" | bash -s -- install --without-geodata 2>&1 | tail -3
|
|
|
|
|
|
mkdir -p /usr/local/etc/xray /var/log/xray
|
|
|
|
|
|
chown -R nobody:nogroup /var/log/xray 2>/dev/null || true
|
2026-05-18 18:05:32 +03:00
|
|
|
|
cat > /usr/local/etc/xray/config.json <<XCFG
|
2026-05-15 16:57:34 +03:00
|
|
|
|
{
|
|
|
|
|
|
"log": {"loglevel":"warning","access":"/var/log/xray/access.log","error":"/var/log/xray/error.log"},
|
|
|
|
|
|
"dns": {"servers":["https+local://1.1.1.1/dns-query","1.1.1.1"],"queryStrategy":"UseIP"},
|
2026-05-18 22:11:45 +03:00
|
|
|
|
"inbounds": [{"tag":"reality-${ALIAS}","listen":"0.0.0.0","port":443,"protocol":"vless",
|
|
|
|
|
|
"settings":{"clients":[{"id":"${UUID}","email":"montana-universal","flow":"xtls-rprx-vision"}],"decryption":"none"},
|
2026-05-15 16:57:34 +03:00
|
|
|
|
"streamSettings":{"network":"tcp","security":"reality",
|
2026-05-18 22:11:45 +03:00
|
|
|
|
"realitySettings":{"show":false,"dest":"${SNI}:443","xver":0,
|
|
|
|
|
|
"serverNames":["${SNI}"],"privateKey":"${PRIV}","shortIds":["${SID}"]}},
|
2026-05-15 16:57:34 +03:00
|
|
|
|
"sniffing":{"enabled":true,"destOverride":["http","tls","quic"]}}],
|
|
|
|
|
|
"outbounds":[{"tag":"direct","protocol":"freedom","settings":{"domainStrategy":"UseIP"}},
|
|
|
|
|
|
{"tag":"blocked","protocol":"blackhole"},{"tag":"dns-out","protocol":"dns"}],
|
|
|
|
|
|
"routing":{"rules":[{"type":"field","port":"53","outboundTag":"dns-out"},
|
|
|
|
|
|
{"type":"field","ip":["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8"],"outboundTag":"blocked"},
|
|
|
|
|
|
{"type":"field","protocol":["bittorrent"],"outboundTag":"blocked"}]}
|
|
|
|
|
|
}
|
|
|
|
|
|
XCFG
|
2026-05-18 18:05:32 +03:00
|
|
|
|
xray -test -config /usr/local/etc/xray/config.json >/dev/null
|
2026-05-18 22:11:45 +03:00
|
|
|
|
mkdir -p /etc/systemd/system/xray.service.d
|
|
|
|
|
|
cat > /etc/systemd/system/xray.service.d/30-autorestart.conf <<EOF
|
|
|
|
|
|
[Service]
|
|
|
|
|
|
Restart=always
|
|
|
|
|
|
RestartSec=3
|
|
|
|
|
|
ExecStopPost=/usr/local/bin/montana-vpn-deregister
|
|
|
|
|
|
[Unit]
|
|
|
|
|
|
StartLimitIntervalSec=300
|
|
|
|
|
|
StartLimitBurst=10
|
|
|
|
|
|
EOF
|
|
|
|
|
|
cat > /usr/local/bin/montana-vpn-deregister <<DRG
|
|
|
|
|
|
#!/bin/sh
|
|
|
|
|
|
# Удаляет наш IP из cdn.montana.quest multi-A при штатной остановке xray.
|
|
|
|
|
|
[ -f /etc/montana/orchestrator-token ] || exit 0
|
|
|
|
|
|
IP=\$(curl -s --max-time 5 https://api.ipify.org)
|
|
|
|
|
|
TOKEN=\$(cat /etc/montana/orchestrator-token)
|
|
|
|
|
|
curl -s --max-time 10 -X POST -H "Content-Type: application/json" \
|
|
|
|
|
|
--data "{\"ip\":\"\${IP}\",\"secret\":\"\${TOKEN}\"}" \
|
|
|
|
|
|
"${ORCH}/deregister" >/dev/null 2>&1 || true
|
|
|
|
|
|
DRG
|
|
|
|
|
|
chmod 755 /usr/local/bin/montana-vpn-deregister
|
|
|
|
|
|
if [ -n "${TOKEN:-}" ]; then
|
|
|
|
|
|
install -m 0600 /dev/stdin /etc/montana/orchestrator-token <<<"$TOKEN"
|
|
|
|
|
|
fi
|
|
|
|
|
|
systemctl daemon-reload
|
2026-05-18 18:05:32 +03:00
|
|
|
|
systemctl enable --now xray
|
|
|
|
|
|
fi
|
2026-05-15 16:57:34 +03:00
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
# 5. Регистрация в orchestrator
|
2026-05-15 16:47:16 +03:00
|
|
|
|
LAT="$(echo "$COORDS" | cut -d, -f1)"
|
|
|
|
|
|
LON="$(echo "$COORDS" | cut -d, -f2)"
|
2026-05-18 18:05:32 +03:00
|
|
|
|
|
|
|
|
|
|
if [ "$ROLE" = "vpn-backend" ]; then
|
|
|
|
|
|
ENDPOINT="$ORCH/register"
|
|
|
|
|
|
PAYLOAD=$(jq -nc \
|
|
|
|
|
|
--arg alias "$ALIAS" --arg ip "$PUBLIC_IP" --arg country "$COUNTRY" \
|
|
|
|
|
|
--arg hosting "$HOSTING" --arg label "$LABEL" --argjson lat "$LAT" --argjson lon "$LON" \
|
|
|
|
|
|
--arg pbk "$PBK" --arg uuid "$UUID" --arg sid "$SID" --arg role "$ROLE" --arg secret "$TOKEN" \
|
|
|
|
|
|
'{alias:$alias,ip:$ip,country:$country,hosting:$hosting,label:$label,coords:[$lat,$lon],reality_pbk:$pbk,reality_uuid:$uuid,reality_sid:$sid,role:$role,secret:$secret}')
|
|
|
|
|
|
else
|
|
|
|
|
|
ENDPOINT="$ORCH/announce"
|
|
|
|
|
|
PAYLOAD=$(jq -nc \
|
|
|
|
|
|
--arg alias "$ALIAS" --arg ip "$PUBLIC_IP" --arg country "$COUNTRY" \
|
|
|
|
|
|
--arg hosting "$HOSTING" --arg label "$LABEL" --argjson lat "$LAT" --argjson lon "$LON" \
|
|
|
|
|
|
'{alias:$alias,ip:$ip,country:$country,hosting:$hosting,label:$label,coords:[$lat,$lon]}')
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
RESP=$(curl -sk -X POST "$ENDPOINT" -H 'Content-Type: application/json' -d "$PAYLOAD")
|
|
|
|
|
|
echo "orchestrator ($ROLE → $ENDPOINT): $RESP"
|
2026-05-15 16:47:16 +03:00
|
|
|
|
|
|
|
|
|
|
cat <<DONE
|
|
|
|
|
|
|
2026-05-18 18:05:32 +03:00
|
|
|
|
╔══════════════════════════════════════════════════╗
|
|
|
|
|
|
║ Montana Node "$ALIAS" подключён ($ROLE) ║
|
|
|
|
|
|
╠══════════════════════════════════════════════════╣
|
|
|
|
|
|
║ montana-node :8444 — p2p Bootstrap→CandidateVdf
|
|
|
|
|
|
║ $([ "$ROLE" = vpn-backend ] && echo "xray :443 — Reality VPN backend cascade
|
|
|
|
|
|
║ reality_pbk: $PBK" || echo "(VPN не включён — это p2p-only узел)")
|
2026-05-15 16:57:34 +03:00
|
|
|
|
║
|
2026-05-18 18:05:32 +03:00
|
|
|
|
║ На /net/ карте появишься через ~30 сек (pull-aggregator).
|
|
|
|
|
|
$([ "$ROLE" = vpn-backend ] && echo "║ В Helsinki cascade balancer уже добавлен.")
|
|
|
|
|
|
╚══════════════════════════════════════════════════╝
|
2026-05-15 16:47:16 +03:00
|
|
|
|
DONE
|