montana/Montana-Protocol/Code/scripts/install-vps.sh

361 lines
14 KiB
Bash
Raw Normal View History

#!/bin/bash
2026-05-26 21:14:51 +03:00
# Montana node — one-command install on a clean Linux VPS.
# Montana Ядро 0.1 (mainnet). Все узлы равны: после запуска новый узел
# подключается к bootstrap peers и становится полноправным участником сети,
# как в Bitcoin. Нет привилегированных «генезис-узлов».
#
2026-05-26 21:14:51 +03:00
# Usage (one line on the VPS):
# curl -sSL https://raw.githubusercontent.com/efir369999/Montana/main/Code/scripts/install-vps.sh | sudo bash
#
2026-05-26 21:14:51 +03:00
# Or after `git clone`:
# sudo bash /opt/montana/Code/scripts/install-vps.sh
#
2026-05-26 21:14:51 +03:00
# Defaults:
# - listen on /ip4/0.0.0.0/tcp/8444 (Noise_PQ XX over TCP)
# - dial the bootstrap nodes from Code/scripts/genesis-manifest.json
# - the new node joins as a candidate; live mesh heartbeats appear in the
# bootstrap peers' logs within seconds of `systemctl start`
#
# Overrides:
# MONTANA_LISTEN=/ip4/0.0.0.0/tcp/PORT change listen port (default 8444)
# MONTANA_GENESIS_MANIFEST=/path/to/file use a custom manifest file
# MONTANA_REPO_BRANCH=main override branch (default main)
# INSTALL_VPN=1 also install Xray Reality VPN
# backend on :443 (joins the
# federated /vpn/sub pool)
#
# Steps:
# 1. Verify root and detect OS (Ubuntu / Debian / Fedora / RHEL / Alpine)
# 2. Install system build dependencies
# 3. Install Rust toolchain via rustup (skip if cargo already present)
# 4. Clone or fast-forward the repository at /opt/montana
# 5. Build the release binary
# 6. Create system user `montana` and data directory /var/lib/montana
# 7. Generate identity (prints 24-word recovery mnemonic — save it!)
# 8. Deploy /etc/montana/genesis-manifest.json from the repo bundle
# 9. Install systemd unit with hardening + cross-machine networking
# 10. Enable and start the service
#
# After step 10, the local node dials the bootstrap peers, negotiates
# Noise_PQ XX (/montana/noise-pq-xx/1.0.0), and starts exchanging Ping/Pong
# heartbeats. The new node appears in mos/fra/zel journals as
# `[network] CONNECTION ESTABLISHED peer=<your XX peer_id> label=unknown`.
set -euo pipefail
REPO_URL="${MONTANA_REPO_URL:-https://github.com/efir369999/Montana.git}"
REPO_BRANCH="${MONTANA_REPO_BRANCH:-main}"
INSTALL_DIR="/opt/montana"
DATA_DIR="/var/lib/montana"
2026-05-26 21:14:51 +03:00
ETC_DIR="/etc/montana"
BIN_DST="/usr/local/bin/montana-node"
USER_NAME="montana"
SERVICE_FILE="/etc/systemd/system/montana-node.service"
2026-05-26 21:14:51 +03:00
DEFAULT_LISTEN="/ip4/0.0.0.0/tcp/8444"
DEFAULT_MANIFEST_SRC="$INSTALL_DIR/Code/scripts/genesis-manifest.json"
DEFAULT_MANIFEST_DST="$ETC_DIR/genesis-manifest.json"
2026-05-26 21:14:51 +03:00
log() { printf '\033[1;32m[install-vps]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[install-vps]\033[0m %s\n' "$*" >&2; }
2026-05-26 21:14:51 +03:00
die() { printf '\033[1;31m[install-vps] ERROR:\033[0m %s\n' "$*" >&2; exit 1; }
2026-05-26 21:14:51 +03:00
# Step 1: root + OS detection
if [ "$(id -u)" != "0" ]; then
2026-05-26 21:14:51 +03:00
die "root privileges required. Run: curl -sSL <URL> | sudo bash"
fi
if [ ! -f /etc/os-release ]; then
2026-05-26 21:14:51 +03:00
die "cannot detect OS — /etc/os-release missing"
fi
. /etc/os-release
OS_ID="${ID:-unknown}"
2026-05-26 21:14:51 +03:00
log "detected OS: ${PRETTY_NAME:-$OS_ID}"
2026-05-26 21:14:51 +03:00
# Step 2: system deps
log "installing system dependencies..."
case "$OS_ID" in
ubuntu|debian)
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq build-essential clang pkg-config git curl perl ca-certificates >/dev/null
;;
fedora|rhel|centos|rocky|almalinux)
dnf install -y -q gcc gcc-c++ clang pkgconf-pkg-config git curl perl ca-certificates make >/dev/null
;;
alpine)
apk add --no-cache build-base clang pkgconfig git curl perl linux-headers ca-certificates >/dev/null
;;
*)
2026-05-26 21:14:51 +03:00
die "unsupported OS: $OS_ID. Supported: ubuntu, debian, fedora, rhel, centos, rocky, almalinux, alpine"
;;
esac
2026-05-26 21:14:51 +03:00
# Step 3: Rust toolchain
if ! command -v cargo >/dev/null 2>&1; then
2026-05-26 21:14:51 +03:00
log "installing Rust toolchain via rustup..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal --no-modify-path
export PATH="/root/.cargo/bin:$PATH"
else
2026-05-26 21:14:51 +03:00
log "Rust toolchain present: $(cargo --version)"
fi
export PATH="${HOME:-/root}/.cargo/bin:/root/.cargo/bin:$PATH"
2026-05-26 21:14:51 +03:00
# Step 4: clone / update repo
if [ -d "$INSTALL_DIR/.git" ]; then
2026-05-26 21:14:51 +03:00
log "updating repository $INSTALL_DIR..."
cd "$INSTALL_DIR"
git fetch origin "$REPO_BRANCH"
git reset --hard "origin/$REPO_BRANCH"
else
2026-05-26 21:14:51 +03:00
log "cloning $REPO_URL (branch $REPO_BRANCH) → $INSTALL_DIR..."
rm -rf "$INSTALL_DIR"
git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$INSTALL_DIR"
fi
2026-05-26 21:14:51 +03:00
# Step 5: build release binary
SOURCE_DIR="$INSTALL_DIR/Code"
if [ ! -d "$SOURCE_DIR" ]; then
2026-05-26 21:14:51 +03:00
die "expected directory '$SOURCE_DIR' not found in repository"
fi
cd "$SOURCE_DIR"
2026-05-26 21:14:51 +03:00
log "building montana-node release (530 minutes on first run)..."
cargo build --release -p montana-node 2>&1 | tail -5
2026-05-26 21:14:51 +03:00
# Step 6: install binary
install -m 0755 target/release/montana-node "$BIN_DST"
2026-05-26 21:14:51 +03:00
log "binary installed: $BIN_DST"
2026-05-26 21:14:51 +03:00
# Step 7: system user + data dir
if ! id "$USER_NAME" >/dev/null 2>&1; then
2026-05-26 21:14:51 +03:00
log "creating system user $USER_NAME..."
useradd -r -s /usr/sbin/nologin -d "$DATA_DIR" -M "$USER_NAME" 2>/dev/null \
|| useradd -r -s /bin/false -d "$DATA_DIR" "$USER_NAME"
fi
mkdir -p "$DATA_DIR"
chown -R "$USER_NAME:$USER_NAME" "$DATA_DIR"
chmod 0750 "$DATA_DIR"
2026-05-26 21:14:51 +03:00
# Step 8: identity (only if missing)
if [ ! -f "$DATA_DIR/identity.bin" ]; then
2026-05-26 21:14:51 +03:00
log "generating identity (24-word mnemonic)..."
echo
echo "================================================================"
2026-05-26 21:14:51 +03:00
echo " NOTE: 24 mnemonic words will be printed below."
echo " Write them down in a safe place — this is the entire backup."
echo " Lose them → lose the node and all earned Ɉ."
echo "================================================================"
echo
sudo -u "$USER_NAME" "$BIN_DST" init --data-dir "$DATA_DIR"
echo
else
2026-05-26 21:14:51 +03:00
log "identity already exists ($DATA_DIR/identity.bin) — skipping init"
fi
2026-05-26 21:14:51 +03:00
# Step 9: deploy Genesis manifest
LISTEN_ADDR="${MONTANA_LISTEN:-$DEFAULT_LISTEN}"
MANIFEST_PATH="${MONTANA_GENESIS_MANIFEST:-$DEFAULT_MANIFEST_DST}"
mkdir -p "$ETC_DIR"
chmod 0755 "$ETC_DIR"
if [ -z "${MONTANA_GENESIS_MANIFEST:-}" ]; then
# default path: copy the bundled manifest to /etc/montana
if [ ! -f "$DEFAULT_MANIFEST_SRC" ]; then
die "expected bundled manifest at $DEFAULT_MANIFEST_SRC not found in repository"
fi
2026-05-26 21:14:51 +03:00
install -m 0644 "$DEFAULT_MANIFEST_SRC" "$DEFAULT_MANIFEST_DST"
log "deployed default Genesis manifest to $DEFAULT_MANIFEST_DST"
else
2026-05-26 21:14:51 +03:00
if [ ! -f "$MANIFEST_PATH" ]; then
warn "custom manifest $MANIFEST_PATH not yet present — node will retry on systemd restart loop"
else
log "using custom manifest at $MANIFEST_PATH"
fi
fi
2026-05-26 21:14:51 +03:00
# Step 10: systemd unit
log "installing systemd unit at $SERVICE_FILE..."
cat > "$SERVICE_FILE" <<UNIT
[Unit]
2026-05-26 21:14:51 +03:00
Description=Montana Node (cross-machine, Proof-of-Time, Noise_PQ XX)
Documentation=https://github.com/efir369999/Montana
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=$USER_NAME
Group=$USER_NAME
2026-05-26 21:14:51 +03:00
ExecStart=$BIN_DST start --data-dir $DATA_DIR --listen $LISTEN_ADDR --genesis-manifest $MANIFEST_PATH
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
2026-05-26 21:14:51 +03:00
# Hardening per systemd security best-practice
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=$DATA_DIR
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=no
SystemCallArchitectures=native
2026-05-26 21:14:51 +03:00
# Resource limits — node is single-threaded, one core is enough
CPUQuota=110%
LimitNOFILE=4096
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable montana-node.service >/dev/null 2>&1
systemctl restart montana-node.service
2026-05-26 21:14:51 +03:00
# Final report
sleep 3
log ""
log "================================================================"
2026-05-26 21:14:51 +03:00
log " INSTALL COMPLETE"
log "================================================================"
log ""
2026-05-26 21:14:51 +03:00
log "Binary: $BIN_DST"
log "Data: $DATA_DIR"
log "User: $USER_NAME"
log "Listen: $LISTEN_ADDR"
log "Manifest: $MANIFEST_PATH"
log "Service: montana-node.service"
log ""
2026-05-26 21:14:51 +03:00
log "--- service status ---"
systemctl --no-pager status montana-node.service | head -10 || true
log ""
2026-05-26 21:14:51 +03:00
log "Useful commands:"
log " systemctl status montana-node # current status"
log " journalctl -u montana-node -f # follow logs"
log " systemctl stop montana-node # stop the node"
log " systemctl restart montana-node # restart"
log " $BIN_DST status --data-dir $DATA_DIR # phase + balance"
log ""
2026-05-26 21:14:51 +03:00
log "Node lifecycle:"
log " Phase 1: Bootstrap → CandidateVdf (sequential SHA-256 chain to vdf_chain_length ≥ τ₂)"
log " Phase 2: CandidateVdf → Registered (NodeRegistration via canonical apply_*)"
log " Phase 3: Registered → Active (selection event on next W where W % 336 == 0)"
log " Phase 4: Active (emission 13 Ɉ per window via apply_proposal)"
log ""
2026-05-26 21:14:51 +03:00
log "This is the spec-defined Sybil barrier — there is no shortcut."
log "The node survives VPS restarts (Restart=on-failure) and resumes at the same window."
log ""
log "Within seconds of start, your node negotiates Noise_PQ XX with the three"
log "Genesis peers (moscow / frankfurt / helsinki) listed in the manifest. To"
log "confirm the connection appears in the live mesh, ask one of the Genesis"
log "operators to grep their journal for your local XX PeerId."
# ───────────────────────────────────────────────────────────────────────
# Optional: Montana Xray Reality VPN backend
# ───────────────────────────────────────────────────────────────────────
# Run with INSTALL_VPN=1 to also stand up the Reality VPN endpoint on :443
# alongside montana-node. The endpoint joins the federated /vpn/sub pool
# served at https://montana.quest/vpn/sub. Universal shared keypair is
# used so every Montana VPN-backend node accepts the same client config
# (see project_montana_vpn_universal_key.md).
#
# Defaults (overridable via env):
# INSTALL_VPN=0 — set to 1 to install
# VPN_UNIVERSAL_UUID — shared UUID for all Montana VPN clients
# VPN_UNIVERSAL_PRIVKEY — shared Reality x25519 private key
# VPN_UNIVERSAL_SID — shared Reality short_id
# VPN_SNI — Reality dest SNI
if [ "${INSTALL_VPN:-0}" = "1" ]; then
log "--- installing Xray Reality VPN backend on :443 ---"
VPN_UNIVERSAL_UUID="${VPN_UNIVERSAL_UUID:-e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d}"
VPN_UNIVERSAL_PRIVKEY="${VPN_UNIVERSAL_PRIVKEY:-cL7D6FCqH5nWcQlHCKH9uNr-RNwCt5peRAqt8tl9mXs}"
VPN_UNIVERSAL_SID="${VPN_UNIVERSAL_SID:-302805bc0c25e504}"
VPN_SNI="${VPN_SNI:-www.googletagmanager.com}"
XRAY_VERSION="${XRAY_VERSION:-26.2.6}"
if ! command -v xray >/dev/null 2>&1; then
log " installing xray-core v${XRAY_VERSION}..."
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" \
@ install --version "${XRAY_VERSION}" -u root >/dev/null 2>&1 || \
log " WARN xray install failed; continuing"
fi
if command -v xray >/dev/null 2>&1; then
mkdir -p /usr/local/etc/xray /var/log/xray /var/lib/montana-net
cat > /usr/local/etc/xray/config.json <<XRAY
{
"log": {"loglevel": "warning", "access": "/var/log/xray/access.log", "error": "/var/log/xray/error.log"},
"dns": {"servers": ["1.1.1.1", "8.8.8.8"], "queryStrategy": "UseIP"},
"inbounds": [{
"tag": "reality-entry",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [{"id": "${VPN_UNIVERSAL_UUID}", "email": "montana-universal", "flow": "xtls-rprx-vision"}],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "${VPN_SNI}:443",
"xver": 0,
"serverNames": ["${VPN_SNI}"],
"privateKey": "${VPN_UNIVERSAL_PRIVKEY}",
"shortIds": ["${VPN_UNIVERSAL_SID}"]
}
},
"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": ["geoip:private"], "outboundTag": "blocked"}
]}
}
XRAY
# Public-safe metadata file for federated /vpn/sub aggregator
VPN_PUBKEY=$(xray x25519 -i "${VPN_UNIVERSAL_PRIVKEY}" 2>/dev/null | awk -F': ' '/PublicKey/ {print $2}')
cat > /var/lib/montana-net/my-vpn.json <<META
{
"UUID": "${VPN_UNIVERSAL_UUID}",
"PBK": "${VPN_PUBKEY}",
"SID": "${VPN_UNIVERSAL_SID}",
"SNI": "${VPN_SNI}"
}
META
chmod 644 /var/lib/montana-net/my-vpn.json
systemctl daemon-reload
systemctl enable xray >/dev/null 2>&1 || true
systemctl restart xray
sleep 2
if systemctl is-active --quiet xray; then
log " xray active on :443"
log " vless://${VPN_UNIVERSAL_UUID}@<this-host>:443?flow=xtls-rprx-vision&security=reality&sni=${VPN_SNI}&pbk=${VPN_PUBKEY}&sid=${VPN_UNIVERSAL_SID}&type=tcp"
log ""
log "To enroll this node in https://montana.quest/vpn/sub aggregator,"
log "ask the orchestrator operator to POST to /api/orchestrator/register"
log "with this node's IP, alias, country, and label."
else
log " WARN xray failed to start; check journalctl -u xray"
fi
fi
fi