2026-05-26 21:14:51 +03:00
#!/bin/bash
# Montana — one-command Docker install on a clean Linux VPS.
#
# Result: a Helsinki-equivalent VPN-backend Montana node, fully containerised.
# - montana-node container on host network, p2p :8444
# - xray container on host network, Reality VLESS-XTLS-Vision :443
# - nginx-decoy container on host network, plain HTTP :80
# - ufw host firewall opening 22/80/443/8444
#
# Usage on a clean VPS (root):
# curl -fsSL https://raw.githubusercontent.com/efir369999/Montana/main/Code/scripts/install-docker.sh \
# | sudo bash
#
# Pre-stageable secrets (optional — operator-supplied via scp BEFORE running):
# /etc/montana-vpn/privkey Reality x25519 private key. Presence => the
# node joins the universal-key Montana VPN
# federation (same UUID/PBK/SID/SNI as
# Helsinki/Frankfurt/US). Absence => generate
# fresh standalone keys.
# /etc/montana-vpn/orch-token Orchestrator admin secret. Presence => after
# compose-up the installer POSTs /vpn/node/
# register so this node appears in the public
# https://montana.quest/vpn/sub subscription.
#
# Operator metadata (used by orchestrator register payload; sensible defaults
# from the VPS itself when omitted):
2026-05-27 16:23:42 +03:00
# Country, city and coordinates are auto-detected from the node's public IP
# (ip-api.com) when the variables below are omitted; override only if needed:
2026-05-26 21:14:51 +03:00
# MONTANA_ALIAS=<hostname-short> short lowercase alias
2026-05-27 16:23:42 +03:00
# MONTANA_CITY=<city name> auto from geo-IP; e.g. Yerevan
# MONTANA_LABEL=<human label> auto from city; any UTF-8
# MONTANA_COUNTRY=<two-letter ISO> auto from geo-IP; e.g. AM, FI, DE
2026-05-26 21:14:51 +03:00
# MONTANA_HOSTING=<provider name> e.g. WorkTitans
2026-05-27 16:23:42 +03:00
# MONTANA_COORDS='lat,lon' auto from geo-IP; e.g. 40.18,44.51
2026-05-26 21:14:51 +03:00
#
# Other environment knobs:
# MONTANA_DECOY_HOST=www.googletagmanager.com Reality dest SNI
# MONTANA_CLIENT_EMAIL=montana-universal xray client email tag
# MONTANA_NODE_TAG=$(hostname) inbound tag suffix
# MONTANA_REPO_URL=https://github.com/efir369999/Montana.git
# MONTANA_REPO_BRANCH=main
# MONTANA_WIPE_LEGACY=1 purge prior native systemd install
# MONTANA_ORCH_URL=https://montana.quest/vpn/node
# MONTANA_SKIP_VERIFY=0 skip post-install self-checks
set -euo pipefail
# ── configuration ───────────────────────────────────────────────────────────
REPO_URL = " ${ MONTANA_REPO_URL :- https : //github.com/efir369999/Montana.git } "
REPO_BRANCH = " ${ MONTANA_REPO_BRANCH :- main } "
INSTALL_DIR = "/opt/montana"
RUNTIME_DIR = " $INSTALL_DIR /Code/docker/runtime "
VPN_DIR = "/etc/montana-vpn"
VPN_PRIVKEY_FILE = " $VPN_DIR /privkey "
ORCH_TOKEN_FILE = " $VPN_DIR /orch-token "
XRAY_CONF = " $VPN_DIR /xray-config.json "
NGX_CONF = " $VPN_DIR /nginx-decoy.conf "
DECOY_HTML = " $VPN_DIR /decoy-index.html "
DECOY_HOST = " ${ MONTANA_DECOY_HOST :- www .googletagmanager.com } "
CLIENT_EMAIL = " ${ MONTANA_CLIENT_EMAIL :- montana -universal } "
NODE_TAG = " ${ MONTANA_NODE_TAG :- $( hostname -s 2>/dev/null || echo node) } "
WIPE_LEGACY = " ${ MONTANA_WIPE_LEGACY :- 1 } "
ORCH_URL = " ${ MONTANA_ORCH_URL :- https : //montana.quest/vpn/node } "
SKIP_VERIFY = " ${ MONTANA_SKIP_VERIFY :- 0 } "
# Universal Montana VPN client metadata — public, distributed in VLESS subs.
UNIVERSAL_UUID = "e6d355e2-2d79-4c96-a373-3b0e6b6f4b0d"
UNIVERSAL_SID = "302805bc0c25e504"
2026-05-27 16:23:42 +03:00
UNIVERSAL_PRIVKEY = "cL7D6FCqH5nWcQlHCKH9uNr-RNwCt5peRAqt8tl9mXs"
2026-05-26 21:14:51 +03:00
log( ) { printf '\033[1;32m[install-docker]\033[0m %s\n' " $* " ; }
warn( ) { printf '\033[1;33m[install-docker]\033[0m %s\n' " $* " >& 2; }
die( ) { printf '\033[1;31m[install-docker] ERROR:\033[0m %s\n' " $* " >& 2; exit 1; }
ok( ) { printf '\033[1;32m[verify ✓]\033[0m %s\n' " $* " ; }
bad( ) { printf '\033[1;31m[verify ✗]\033[0m %s\n' " $* " >& 2; }
retry( ) {
local n = " $1 " ; shift
local i = 1
while [ " $i " -le " $n " ] ; do
if " $@ " ; then return 0; fi
warn " attempt $i / $n failed for: $* "
i = $(( i+1)) ; sleep $(( i*3))
done
return 1
}
2026-05-27 16:23:42 +03:00
# Built-in orchestrator token — every Montana node auto-registers on install.
BUILTIN_ORCH_TOKEN = "b517e7888473d905d26eba58c444f7cad927978c5ef3a77b5baa8bb6c296c948"
if [ ! -s " $ORCH_TOKEN_FILE " ] ; then
EFFECTIVE_TOKEN = " ${ MONTANA_ORCH_TOKEN :- $BUILTIN_ORCH_TOKEN } "
mkdir -p " $VPN_DIR " && chmod 0700 " $VPN_DIR "
install -m 0600 /dev/stdin " $ORCH_TOKEN_FILE " <<< " $EFFECTIVE_TOKEN "
log " orch-token written ( ${ MONTANA_ORCH_TOKEN : +env override } ${ MONTANA_ORCH_TOKEN :- built -in } ) "
fi
2026-05-26 21:14:51 +03:00
# ── 1. preconditions ─────────────────────────────────────────────────────────
[ " $( id -u) " = "0" ] || die "root privileges required"
[ -f /etc/os-release ] || die "/etc/os-release missing"
. /etc/os-release
OS_ID = " ${ ID :- unknown } "
case " $OS_ID " in
ubuntu| debian) ; ;
*) die " unsupported OS: $OS_ID . Supported: ubuntu, debian " ; ;
esac
log " OS: ${ PRETTY_NAME :- $OS_ID } "
# ── 2. wipe prior native systemd install if any ──────────────────────────────
if [ " $WIPE_LEGACY " = "1" ] ; then
log "wiping any prior native systemd install of montana-node / xray / nginx..."
systemctl stop montana-node xray nginx 2>/dev/null || true
systemctl disable montana-node xray nginx 2>/dev/null || true
rm -f /etc/systemd/system/montana-node.service \
/etc/systemd/system/xray.service /etc/systemd/system/xray@.service
rm -rf /etc/systemd/system/montana-node.service.d \
/etc/systemd/system/xray.service.d /etc/systemd/system/xray@.service.d
systemctl daemon-reload 2>/dev/null || true
systemctl reset-failed 2>/dev/null || true
# Native xray uninstall — only if there's something to remove.
if [ -x /usr/local/bin/xray ] && [ -f /etc/systemd/system/xray.service ] ; then
bash -c " $( curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh) " \
@ remove --purge >/dev/null 2>& 1 || true
fi
rm -f /usr/local/bin/xray /usr/local/bin/xctl
rm -rf /usr/local/etc/xray /usr/local/share/xray /var/log/xray
if dpkg -l 2>/dev/null | grep -qE '^ii nginx' ; then
DEBIAN_FRONTEND = noninteractive apt-get remove --purge -y nginx nginx-core nginx-common nginx-full >/dev/null 2>& 1 || true
DEBIAN_FRONTEND = noninteractive apt-get autoremove -y >/dev/null 2>& 1 || true
fi
rm -rf /etc/nginx /var/www/decoy
if [ -d /var/lib/montana ] ; then
BK = " /root/montana-pre-docker-backup- $( date +%s) "
mkdir -p " $BK "
cp -a /var/lib/montana " $BK / " 2>/dev/null || true
cp -a /etc/montana " $BK / " 2>/dev/null || true
log " backed up legacy data to $BK "
fi
rm -rf /var/lib/montana /etc/montana
id montana >/dev/null 2>& 1 && userdel -r montana 2>/dev/null || true
rm -f /usr/local/bin/montana-node
log "legacy wipe complete"
fi
# ── 3. apt deps + docker + compose plugin ────────────────────────────────────
log "installing apt deps (curl, ca-certificates, gnupg, openssl, jq, ufw, git)..."
export DEBIAN_FRONTEND = noninteractive
retry 3 apt-get update -qq
retry 3 apt-get install -y -qq curl ca-certificates gnupg openssl jq ufw git lsb-release >/dev/null
if ! command -v docker >/dev/null 2>& 1; then
log "installing Docker Engine + compose plugin via official apt repo..."
install -m 0755 -d /etc/apt/keyrings
retry 3 bash -c " curl -fsSL https://download.docker.com/linux/ ${ OS_ID } /gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
chmod a+r /etc/apt/keyrings/docker.gpg
CODENAME = " $( . /etc/os-release && echo " ${ VERSION_CODENAME :- bookworm } " ) "
echo " deb [arch= $( dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/${ OS_ID } ${ CODENAME } stable" > /etc/apt/sources.list.d/docker.list
retry 3 apt-get update -qq
retry 3 apt-get install -y -qq docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin >/dev/null
fi
systemctl enable --now docker >/dev/null 2>& 1 || true
docker --version
docker compose version
# ── 4. firewall ──────────────────────────────────────────────────────────────
log "configuring ufw (22, 80, 443, 8444)..."
ufw allow 22/tcp comment 'SSH' >/dev/null 2>& 1 || true
ufw allow 80/tcp comment 'decoy nginx' >/dev/null 2>& 1 || true
ufw allow 443/tcp comment 'VLESS+TCP+Reality+Vision' >/dev/null 2>& 1 || true
ufw allow 8444/tcp comment 'Montana p2p Noise_PQ XX' >/dev/null 2>& 1 || true
yes | ufw --force enable >/dev/null 2>& 1 || true
# ── 5. clone repo ────────────────────────────────────────────────────────────
if [ -d " $INSTALL_DIR /.git " ] ; then
log " updating repo at $INSTALL_DIR ... "
( cd " $INSTALL_DIR " && retry 3 git fetch --depth 1 origin " $REPO_BRANCH " && \
git reset --hard " origin/ $REPO_BRANCH " )
else
log " cloning $REPO_URL → $INSTALL_DIR ... "
retry 3 git clone --branch " $REPO_BRANCH " --depth 1 " $REPO_URL " " $INSTALL_DIR "
fi
[ -d " $RUNTIME_DIR " ] || die " expected $RUNTIME_DIR not found — repo layout drifted "
2026-05-27 16:23:42 +03:00
# ── 6. xray config — always universal (pre-staged or built-in federation key) ────────────────
2026-05-26 21:14:51 +03:00
mkdir -p " $VPN_DIR " && chmod 0700 " $VPN_DIR "
install -m 0644 " $RUNTIME_DIR /nginx-decoy.conf " " $NGX_CONF "
install -m 0644 " $RUNTIME_DIR /decoy-index.html " " $DECOY_HTML "
if [ -s " $VPN_PRIVKEY_FILE " ] ; then
VPN_MODE = universal
PRIV = " $( tr -d ' \n\r' < " $VPN_PRIVKEY_FILE " ) "
UUID = " $UNIVERSAL_UUID "
SID = " $UNIVERSAL_SID "
log " VPN mode: universal (privkey pre-staged at $VPN_PRIVKEY_FILE ) "
else
2026-05-27 16:23:42 +03:00
VPN_MODE = universal
log "VPN mode: universal (auto — no pre-staged privkey, using built-in federation key)"
PRIV = " $UNIVERSAL_PRIVKEY "
UUID = " $UNIVERSAL_UUID "
SID = " $UNIVERSAL_SID "
2026-05-26 21:14:51 +03:00
install -m 0600 /dev/stdin " $VPN_PRIVKEY_FILE " <<< " $PRIV "
fi
PBK = " $( docker run --rm teddysun/xray:26.2.6 xray x25519 -i " $PRIV " 2>& 1 \
| awk -F': ' '/Password|ublic/ {print $NF; exit}' | tr -d ' \r' ) "
[ -n " $PBK " ] || die "failed to derive PublicKey from PrivateKey"
sed \
-e " s|{{CLIENT_UUID}}| $UUID |g " \
-e " s|{{CLIENT_EMAIL}}| $CLIENT_EMAIL |g " \
-e " s|{{NODE_TAG}}| $NODE_TAG |g " \
-e " s|{{DECOY_HOST}}| $DECOY_HOST |g " \
-e " s|{{REALITY_PRIVATE_KEY}}| $PRIV |g " \
-e " s|{{REALITY_SHORT_ID}}| $SID |g " \
" $RUNTIME_DIR /xray-config.json.template " > " $XRAY_CONF "
chmod 0640 " $XRAY_CONF "
# ── 7. compose up (build + start) ────────────────────────────────────────────
cd " $RUNTIME_DIR "
2026-05-27 16:23:42 +03:00
log "building montana-node image (10-30 min on small VPS)..."
2026-05-26 21:14:51 +03:00
docker compose down --remove-orphans >/dev/null 2>& 1 || true
2026-05-27 16:23:42 +03:00
BUILD_LOG = /var/log/montana-compose.log
: > " $BUILD_LOG "
( BUILDKIT_PROGRESS = plain docker compose build >" $BUILD_LOG " 2>& 1; echo $? >/tmp/.mt_build_rc ) &
BPID = $!
BSTART = $( date +%s) ; BEST = 380; SPIN = '|/-\' ; SI = 0
if [ -t 1 ] ; then
while kill -0 " $BPID " 2>/dev/null; do
N = $( grep -c 'Compiling ' " $BUILD_LOG " 2>/dev/null) || N = 0
CUR = $( grep 'Compiling ' " $BUILD_LOG " 2>/dev/null | tail -1 | sed -E 's/.*Compiling ([^ ]+).*/\1/' ) || CUR = ""
EL = $(( $( date +%s) - BSTART ))
PCT = $(( N * 100 / BEST )) ; if [ " $PCT " -gt 99 ] ; then PCT = 99; fi
FILLED = $(( PCT * 28 / 100 ))
BAR = " $( printf '%*s' " $FILLED " '' | tr ' ' '#' ) $( printf '%*s' $(( 28 - FILLED)) '' | tr ' ' '.' ) "
printf '\r\033[1;32m[build]\033[0m [%s] %3d%% %3dc %5ds %c %-22.22s' " $BAR " " $PCT " " $N " " $EL " " ${ SPIN : $SI : 1 } " " ${ CUR :- preparing } "
SI = $(( ( SI + 1 ) % 4 )) ; sleep 2
done
printf '\r\033[K'
fi
wait " $BPID " 2>/dev/null || true
BRC = $( cat /tmp/.mt_build_rc 2>/dev/null || echo 1) ; rm -f /tmp/.mt_build_rc
NC = $( grep -c 'Compiling ' " $BUILD_LOG " 2>/dev/null) || NC = 0
[ " $BRC " = 0 ] || die " image build failed (rc= $BRC ) — see $BUILD_LOG ; tail: $( tail -20 " $BUILD_LOG " ) "
log " image built ( $NC crates in $(( $( date +%s) - BSTART )) s). starting stack... "
docker compose up -d 2>& 1 | tail -20
2026-05-26 21:14:51 +03:00
# ── 8. wait for identity ─────────────────────────────────────────────────────
log "waiting up to 5 min for montana-node to write identity.bin..."
i = 0
while [ " $i " -lt 60 ] ; do
if docker exec montana-node test -f /var/lib/montana/identity.bin 2>/dev/null; then
break
fi
i = $(( i+1)) ; sleep 5
done
docker exec montana-node test -f /var/lib/montana/identity.bin \
|| die "identity.bin not created after 5 min — inspect: docker logs montana-node"
# ── 9. orchestrator register (only when token + universal mode present) ─────
PUBLIC_IP = " $( curl -fs --max-time 8 https://api.ipify.org || echo '' ) "
2026-05-27 16:23:42 +03:00
# geo-IP self-identification: derive country / city / coordinates from the
# node's public address so the operator never types a city name by hand.
GEO_CC = '' ; GEO_CITY = '' ; GEO_LAT = '' ; GEO_LON = ''
if [ -n " $PUBLIC_IP " ] ; then
GEO_JSON = " $( curl -fs --max-time 8 " http://ip-api.com/json/ ${ PUBLIC_IP } ?fields=status,countryCode,city,lat,lon&lang=ru " || echo '' ) "
if [ -n " $GEO_JSON " ] && [ " $( printf '%s' " $GEO_JSON " | jq -r '.status' 2>/dev/null) " = "success" ] ; then
GEO_CC = " $( printf '%s' " $GEO_JSON " | jq -r '.countryCode // empty' 2>/dev/null) "
GEO_CITY = " $( printf '%s' " $GEO_JSON " | jq -r '.city // empty' 2>/dev/null) "
GEO_LAT = " $( printf '%s' " $GEO_JSON " | jq -r '.lat // empty' 2>/dev/null) "
GEO_LON = " $( printf '%s' " $GEO_JSON " | jq -r '.lon // empty' 2>/dev/null) "
log " geo-IP: country= $GEO_CC city= $GEO_CITY coords= $GEO_LAT , $GEO_LON "
fi
fi
2026-05-26 21:14:51 +03:00
ORCH_RESP = ''
if [ -s " $ORCH_TOKEN_FILE " ] && [ " $VPN_MODE " = "universal" ] && [ -n " $PUBLIC_IP " ] ; then
TOKEN = " $( tr -d ' \n\r' < " $ORCH_TOKEN_FILE " ) "
ALIAS = " ${ MONTANA_ALIAS :- $NODE_TAG } "
2026-05-27 16:23:42 +03:00
COUNTRY = " ${ MONTANA_COUNTRY :- ${ GEO_CC :- XX } } "
CITY = " ${ MONTANA_CITY :- ${ GEO_CITY :- ${ ALIAS ^ } } } "
LABEL = " ${ MONTANA_LABEL :- $CITY } "
2026-05-26 21:14:51 +03:00
HOSTING = " ${ MONTANA_HOSTING :- unknown } "
2026-05-27 16:23:42 +03:00
COORDS = " ${ MONTANA_COORDS :- ${ GEO_LAT :- 0 } , ${ GEO_LON :- 0 } } "
2026-05-26 21:14:51 +03:00
LAT = " $( echo " $COORDS " | cut -d, -f1) "
LON = " $( echo " $COORDS " | cut -d, -f2) "
log " registering with orchestrator at $ORCH_URL /register (alias= $ALIAS , country= $COUNTRY )... "
# Give xray a moment so the Reality probe by the orchestrator succeeds.
sleep 4
PAYLOAD = $( jq -nc \
--arg alias " $ALIAS " --arg ip " $PUBLIC_IP " --arg country " $COUNTRY " \
2026-05-27 16:23:42 +03:00
--arg city " $CITY " \
2026-05-26 21:14:51 +03:00
--arg hosting " $HOSTING " --arg label " $LABEL " --argjson lat " $LAT " --argjson lon " $LON " \
--arg pbk " $PBK " --arg uuid " $UUID " --arg sid " $SID " --arg secret " $TOKEN " \
2026-05-27 16:23:42 +03:00
'{alias:$alias,ip:$ip,country:$country,city:$city,hosting:$hosting,label:$label,coords:[$lat,$lon],reality_pbk:$pbk,reality_uuid:$uuid,reality_sid:$sid,secret:$secret}' )
for _attempt in 1 2 3; do
ORCH_RESP = " $( curl -sk --max-time 20 -X POST -H 'Content-Type: application/json' -d " $PAYLOAD " " $ORCH_URL /register " 2>/dev/null || true ) "
if [ -n " $ORCH_RESP " ] && printf '%s' " $ORCH_RESP " | jq -e '.alias' >/dev/null 2>& 1; then
break
fi
log " orchestrator attempt $_attempt failed, retrying in 5s... "
sleep 5
done
2026-05-26 21:14:51 +03:00
log " orchestrator response: $ORCH_RESP "
2026-05-27 16:23:42 +03:00
if command -v jq >/dev/null 2>& 1 && [ -n " $ORCH_RESP " ] ; then
MR = " $( printf '%s' " $ORCH_RESP " | jq -r '.moscow_reachable // empty' 2>/dev/null || true ) "
RTT = " $( printf '%s' " $ORCH_RESP " | jq -r '.moscow_rtt_ms // empty' 2>/dev/null || true ) "
CEN = " $( printf '%s' " $ORCH_RESP " | jq -r '.cascade.enabled // empty' 2>/dev/null || true ) "
CRE = " $( printf '%s' " $ORCH_RESP " | jq -r '.cascade.reason // empty' 2>/dev/null || true ) "
if [ " $MR " = "true" ] ; then log " Moscow cross-check: reachable (TCP :443, RTT ${ RTT } ms) " ; else log "Moscow cross-check: NOT reachable from Moscow datacenter" ; fi
CFRONTS = " $( printf '%s' " $ORCH_RESP " | jq -r '(.cascade.fronts // []) | join(", ")' 2>/dev/null || true ) "
if [ " $CEN " = "true" ] ; then log " Cascade: ENABLED via ${ CFRONTS :- de .montana.quest } (reason: $CRE ) — clients enter at any of 5 fronts, traffic exits on THIS node " ; else log "Cascade: not needed — direct connection" ; fi
fi
2026-05-26 21:14:51 +03:00
fi
# ── 10. self-verification ───────────────────────────────────────────────────
if [ " $SKIP_VERIFY " != "1" ] ; then
log ""
log "running post-install self-verification..."
PASS = 0; FAIL = 0
vcheck( ) { if eval " $1 " ; then ok " $2 " ; PASS = $(( PASS+1)) ; else bad " $2 " ; FAIL = $(( FAIL+1)) ; fi ; }
vcheck "docker ps --format '{{.Names}} {{.Status}}' | grep -q 'montana-node.*healthy\|montana-node.*Up'" \
"container montana-node up"
vcheck "docker ps --format '{{.Names}} {{.Status}}' | grep -q 'montana-xray.*Up'" \
"container montana-xray up"
vcheck "docker ps --format '{{.Names}} {{.Status}}' | grep -q 'montana-nginx-decoy.*Up'" \
"container montana-nginx-decoy up"
# outbound peer TCP probe — bootstrap peers from genesis manifest
if [ -f " $INSTALL_DIR /Code/scripts/genesis-manifest.json " ] ; then
PEER_HOSTS = " $( jq -r '.peers[] | .multiaddr' " $INSTALL_DIR /Code/scripts/genesis-manifest.json " \
| sed -nE 's|/ip4/([0-9.]+)/tcp/([0-9]+)|\1 \2|p' ) "
while read -r ph pp; do
[ -z " $ph " ] && continue
vcheck " timeout 5 bash -c '</dev/tcp/ $ph / $pp ' 2>/dev/null " \
" peer TCP reachable: $ph : $pp "
done <<< " $PEER_HOSTS "
fi
# local Reality TLS handshake to :443
vcheck " echo Q | timeout 8 openssl s_client -connect 127.0.0.1:443 -servername ' $DECOY_HOST ' -brief 2>&1 | grep -q 'CONNECTION ESTABLISHED' " \
"local TLS handshake :443 via Reality cover SNI"
# decoy :80
vcheck "curl -sf --max-time 8 -o /dev/null 'http://127.0.0.1/'" "decoy :80 returns 200"
# ESTABLISHED peer connections from montana-node
EST = " $( ss -tnp 2>/dev/null | grep montana-node | wc -l) "
vcheck " [ ' $EST ' -ge 1 ] " " at least 1 ESTABLISHED p2p connection (got $EST ) "
# /vpn/sub membership (universal mode only — fresh keys aren't aggregated)
if [ " $VPN_MODE " = "universal" ] && [ -n " $PUBLIC_IP " ] ; then
SUB = " $( curl -sk --max-time 10 " ${ ORCH_URL %/node } /sub " | base64 -d 2>/dev/null || true ) "
ALIAS_LOWER = " $( echo " ${ MONTANA_ALIAS :- $NODE_TAG } " | tr '[:upper:]' '[:lower:]' ) "
# match either the alias label or this server's public IP appearing in any VLESS URL
vcheck " echo \"\$SUB\" | grep -qi -E ' ${ ALIAS_LOWER } \\.montana\\.quest| ${ PUBLIC_IP //./ \\ . } ' " \
"node appears in https://montana.quest/vpn/sub subscription"
fi
log ""
if [ " $FAIL " = "0" ] ; then
log " self-verification: $PASS / $PASS checks passed "
else
warn " self-verification: $PASS passed / $FAIL failed — review checks above "
fi
fi
# ── 11. final report ────────────────────────────────────────────────────────
log ""
log "================================================================"
log " INSTALL COMPLETE"
log "================================================================"
log ""
log "Containers:"
docker compose ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null || docker compose ps
log ""
log "Montana node identity (24-word mnemonic — write it down NOW):"
echo "----------------------------------------------------------------"
docker exec montana-node cat /var/lib/montana/mnemonic.txt 2>/dev/null \
|| warn "mnemonic.txt not yet flushed — run: docker exec montana-node cat /var/lib/montana/mnemonic.txt"
echo "----------------------------------------------------------------"
log ""
log "VPN client subscription (VLESS Reality):"
echo " vless:// ${ UUID } @ ${ PUBLIC_IP :- <host-ip> } :443?encryption=none&flow=xtls-rprx-vision&security=reality&sni= ${ DECOY_HOST } &fp=chrome&pbk= ${ PBK } &sid= ${ SID } &type=tcp#montana- ${ NODE_TAG } "
log ""
if [ -n " $ORCH_RESP " ] ; then
log " Orchestrator: $ORCH_RESP "
log " Public subscription (decoded): curl -sk ${ ORCH_URL %/node } /sub | base64 -d "
fi
log ""
log "Useful commands:"
log " docker compose -f $RUNTIME_DIR /docker-compose.yml ps "
log " docker logs -f montana-node"
log " docker exec montana-node /usr/local/bin/montana-node status --data-dir /var/lib/montana"
log " docker logs montana-xray"
log " docker compose -f $RUNTIME_DIR /docker-compose.yml down # stop "
log " docker compose -f $RUNTIME_DIR /docker-compose.yml down -v # stop + wipe identity "
log ""
log "Node lifecycle:"
log " Phase 1: CandidateVdf — sequential SHA-256 chain to vdf_chain_length >= τ₂"
log " Phase 2: Registered — NodeRegistration via apply_proposal on next selection window"
log " Phase 3: Active — emission 13 Ɉ per window via apply_proposal"