#!/bin/sh
# Разовый установщик NetBird-агента DSA для старых Mac — macOS 10.15 и 11 на Intel, куда официальные сборки не встают. Вне стандартного пути: лежит на d.dsa.one как есть, не поддерживается; понадобится новая версия — пересобрать скриптами рядом и вписать её в LEGACY_*.
# Штатный установщик для macOS 12 и новее — https://nb.dsa.one/install.
#   sudo -v && curl -fsSL https://d.dsa.one/netbird/macos/install-netbird-legacy.sh | sudo sh
# Без ключа машина уходит в общую группу UNASSIGNED и ждёт, пока её разложат по клиенту на сервере; свой ключ — аргументом: ... | sudo sh -s -- '<setup-key>'.
# sudo -v стоит отдельно: zsh, оболочка macOS по умолчанию, приостанавливает sudo,
# который спрашивает пароль внутри конвейера, и установка молча встаёт.
# Скрипт приходит через stdin, поэтому внешним командам, которые могут читать stdin, даём </dev/null — иначе они съедят остаток скрипта.
# Сохранять в UTF-8 без BOM: sh не понимает BOM в первой строке.
set -u
# /opt/homebrew/bin — чтобы увидеть агент из Homebrew на Apple Silicon и не поставить поверх второй.
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin
export PATH

MGMT_URL='https://nb.dsa.one'
# Общий ключ группы UNASSIGNED. Скрипт лежит в открытом доступе, поэтому ключ публичен по построению: у группы нет своих правил доступа. Значение сверяется с netbird_keys.yml::netbird_unassigned_setup_key при раскатке.
UNASSIGNED_SETUP_KEY='CCA04C63-44F8-4DC7-B6C6-24C64D9B6BBA'
# Корень macOS только для чтения, стандартный /_dsa там не создать — служебная зона DSA живёт в /usr/local/dsa.
DSA_DIR='/usr/local/dsa'
KEYS_FILE='/etc/ssh/dsa_admins.keys'
SSHD_CONFIG='/etc/ssh/sshd_config'
# 010 — раньше Apple-овского 100-macos.conf: в sshd побеждает первое встреченное значение.
SSHD_DROPIN='/etc/ssh/sshd_config.d/010-dsa.conf'
REFRESH_LABEL='one.dsa.refresh-admin-keys'
REFRESH_PLIST="/Library/LaunchDaemons/$REFRESH_LABEL.plist"
# Наша сборка агента для macOS 10.15 и 11 на Intel; собрать новую — nb_macos_legacy_build.zsh рядом.
LEGACY_VERSION='0.80.0'
LEGACY_URL="https://d.dsa.one/netbird/macos/netbird-$LEGACY_VERSION-dsa-darwin-amd64"
LEGACY_SHA256='a82993f198e4fbeed8bfc083d06fd150d97a8e931081f1661f2af271608ae190'

step() { printf '  -> %s\n' "$1"; }
die() { printf '  -> ОШИБКА: %s\n' "$1" >&2; exit 1; }

printf '=== NetBird DSA для macOS ===\n'
[ "$(uname -s)" = Darwin ] || die "скрипт только для macOS; для Linux — вкладка Linux на странице https://nb.dsa.one/install"
[ "$(id -u)" -eq 0 ] || die "запусти через sudo: sudo -v && curl -fsSL https://d.dsa.one/netbird/macos/install-netbird-legacy.sh | sudo sh"
SETUP_KEY=${1:-$UNASSIGNED_SETUP_KEY}

# Официальные сборки NetBird на Go 1.25 и новее на macOS старше 12 не запускаются: dyld: Symbol not found: _SecTrustCopyCertificateChain, этой функции до macOS 12 нет. Туда ставим нашу сборку того же кода с заплаткой тулчейна Go — без приложения в строке меню, только служба и команда netbird.
osver=$(sw_vers -productVersion)
major=${osver%%.*}
minor=${osver#*.}
minor=${minor%%.*}
case "$major" in
    1[2-9]|[2-9][0-9]) legacy=0 ;;
    11) legacy=1 ;;
    10) [ "$minor" -ge 15 ] || die "macOS $osver слишком старая: агент NetBird работает на macOS 10.15 и новее"; legacy=1 ;;
    *) die "macOS $osver слишком старая: агент NetBird работает на macOS 10.15 и новее" ;;
esac
# Любой Mac на Apple Silicon обновляется до macOS 12, поэтому сборку под arm64 не делаем.
[ "$legacy" -eq 0 ] || [ "$(uname -m)" = x86_64 ] || die "на Apple Silicon с macOS $osver агент не поставить — обнови macOS до 12 или новее"

# Агент — официальный пакет или нашу сборку — останавливаем и убираем; настройки и ключ пира в /var/lib/netbird остаются, пир в mesh тот же.
remove_agent() {
    launchctl bootout system/netbird </dev/null >/dev/null 2>&1
    pkill -f /Applications/NetBird.app </dev/null
    rm -rf /Applications/NetBird.app
    rm -f /usr/local/bin/netbird /usr/local/bin/netbird-ui /Library/LaunchDaemons/netbird.plist /var/run/netbird.sock
    # По квитанции пакета агент решает, что его ставили пакетом, и при автообновлении с сервера скачал бы официальную сборку, которая на 10.15 и 11 не запускается.
    pkgutil --forget io.netbird.client </dev/null >/dev/null 2>&1
    # io.netbird.client_x86_64 — квитанция старых официальных пакетов под Intel: пакет снят, квитанцию убираем вместе с ним.
    pkgutil --forget io.netbird.client_x86_64 </dev/null >/dev/null 2>&1
}

install_official() {
    # Официальный скрипт сам выбирает сборку под Intel или Apple Silicon.
    step "ставлю агент официальным скриптом NetBird"
    curl -fsSL https://pkgs.netbird.io/install.sh | sh || die "установка агента не удалась"
    command -v netbird >/dev/null 2>&1 || die "после установки нет команды netbird"
}

install_legacy() {
    step "ставлю сборку DSA $LEGACY_VERSION для macOS $osver"
    remove_agent
    tmp=$(mktemp /tmp/netbird.XXXXXX) || die "не создать временный файл"
    curl -fsSL -o "$tmp" "$LEGACY_URL" </dev/null || { rm -f "$tmp"; die "не скачать $LEGACY_URL"; }
    [ "$(shasum -a 256 "$tmp" | awk '{print $1}')" = "$LEGACY_SHA256" ] || { rm -f "$tmp"; die "sha256 скачанного агента не совпал с ожидаемым"; }
    mkdir -p /usr/local/bin
    install -m 0755 -o root -g wheel "$tmp" /usr/local/bin/netbird || { rm -f "$tmp"; die "не положить агент в /usr/local/bin"; }
    rm -f "$tmp"
    netbird service install </dev/null || die "не установить службу агента"
    netbird service start </dev/null || die "не запустить службу агента"
    i=0
    until [ -S /var/run/netbird.sock ]; do
        i=$((i + 1))
        [ $i -lt 15 ] || die "служба агента не поднялась за 15 секунд — смотри /var/log/netbird/client.log"
        sleep 1
    done
}

if [ "$legacy" -eq 1 ]; then
    if [ "$(netbird version </dev/null 2>/dev/null)" = "$LEGACY_VERSION" ] && ! pkgutil --pkg-info io.netbird.client </dev/null >/dev/null 2>&1; then
        step "сборка DSA $LEGACY_VERSION уже стоит, установку пропускаю"
    else
        install_legacy
    fi
elif [ -f /usr/local/bin/netbird ] && [ ! -L /usr/local/bin/netbird ] && ! pkgutil --pkg-info io.netbird.client </dev/null >/dev/null 2>&1; then
    # Файл без квитанции io.netbird.client и не ссылка Homebrew — так остаётся наша сборка после обновления macOS с 10.15 или 11. Сама она не обновится, а официальный скрипт поверх работающего агента не ставит, поэтому сначала снимаем её.
    step "меняю сборку DSA на официальный агент: на macOS $osver он работает и обновляется сам"
    remove_agent
    install_official
elif command -v netbird >/dev/null 2>&1; then
    step "агент уже стоит ($(netbird version </dev/null 2>/dev/null)), установку пропускаю"
else
    install_official
fi

step "привязываю к mesh ($MGMT_URL)"
netbird up --management-url "$MGMT_URL" --setup-key "$SETUP_KEY" </dev/null || die "netbird up не прошёл"

NB_IP=''
i=0
while [ $i -lt 30 ]; do
    NB_IP=$(netbird status --ipv4 </dev/null 2>/dev/null)
    case "$NB_IP" in 100.*) break ;; esac
    i=$((i + 1))
    sleep 2
done
case "$NB_IP" in 100.*) step "адрес в mesh: $NB_IP" ;; *) die "агент не получил адрес в mesh за минуту — проверь netbird status" ;; esac

# Ключи админов раздаёт nb.dsa.one только внутри mesh — роль feature_linux_netbird_admin_keys_server.
mkdir -p "$DSA_DIR" || die "не создать $DSA_DIR"
cat > "$DSA_DIR/refresh-admin-keys.sh" <<'EOF'
#!/bin/sh
# Обновляет ключи админов DSA для входа по SSH; ставит install-netbird.sh, запускает launchd раз в сутки.
# Файл перезаписывается только ответом из одних ключей: сбой сети или заглушка вместо файла не должны закрыть вход.
url='http://dsa-nb-12.nb.dsa.one:8080/dsa-admins.keys'
dst='/etc/ssh/dsa_admins.keys'
tmp=$(/usr/bin/mktemp /tmp/dsa-keys.XXXXXX) || exit 1
rc=1
if /usr/bin/curl -fsS -m 30 -o "$tmp" "$url" \
    && /usr/bin/grep -Eq '^(ssh-|sk-ssh-|ecdsa-)' "$tmp" \
    && ! /usr/bin/grep -Evq '^(ssh-|sk-ssh-|ecdsa-|#)|^$' "$tmp"; then
    /usr/bin/install -m 0644 -o root -g wheel "$tmp" "$dst" && rc=0
fi
/bin/rm -f "$tmp"
exit $rc
EOF
chmod 0755 "$DSA_DIR/refresh-admin-keys.sh"

# Сразу после netbird up имя dsa-nb-12.nb.dsa.one может ещё не резолвиться — пробуем 18 раз с паузой 5 секунд.
step "скачиваю ключи админов DSA из mesh"
i=0
until "$DSA_DIR/refresh-admin-keys.sh"; do
    i=$((i + 1))
    [ $i -lt 18 ] || die "ключи админов не скачались за 18 попыток — агент в mesh, но SSH не настроен; повтори команду позже"
    sleep 5
done

cat > "$REFRESH_PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>$REFRESH_LABEL</string>
    <key>ProgramArguments</key><array><string>$DSA_DIR/refresh-admin-keys.sh</string></array>
    <key>StartInterval</key><integer>86400</integer>
    <key>RunAtLoad</key><true/>
</dict>
</plist>
EOF
chown root:wheel "$REFRESH_PLIST"
chmod 0644 "$REFRESH_PLIST"
launchctl bootout "system/$REFRESH_LABEL" </dev/null >/dev/null 2>&1
launchctl bootstrap system "$REFRESH_PLIST" </dev/null || die "не запустить ежедневное обновление ключей ($REFRESH_LABEL)"

# macOS 12+ подключает sshd_config.d сам — туда кладём свой файл. На 10.15 и 11 подключения нет, а их OpenSSH старше 8.2 не понимает и Include, поэтому там настройки ставим в начало sshd_config: в sshd побеждает первое встреченное значение. Без них sshd молча пускал бы по паролю и отовсюду.
# В OpenSSH до 8.7 ввод пароля через PAM включает ChallengeResponseAuthentication,
# KbdInteractiveAuthentication его не выключает; в 8.7+ это одна опция под двумя именами.
settings="# install-netbird.sh DSA: вход по SSH — только ключами, только из mesh NetBird и только в учётки группы admin.
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
AuthorizedKeysFile .ssh/authorized_keys $KEYS_FILE
AllowUsers *@100.64.0.0/10
AllowGroups admin"
if [ "$legacy" -eq 0 ]; then
    grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*' "$SSHD_CONFIG" \
        || die "в $SSHD_CONFIG нет Include /etc/ssh/sshd_config.d/* — агент в mesh, но SSH не настроен"
    printf '%s\n' "$settings" > "$SSHD_DROPIN" || die "не записать $SSHD_DROPIN — SSH не настроен"
else
    # Копию исходного sshd_config снимает первый запуск; повторный строит файл от неё, а не дописывает настройки второй раз.
    [ -f "$SSHD_CONFIG.dsa-backup" ] || cp -p "$SSHD_CONFIG" "$SSHD_CONFIG.dsa-backup" || die "не сохранить копию $SSHD_CONFIG — SSH не настроен"
    { printf '%s\n\n' "$settings"; cat "$SSHD_CONFIG.dsa-backup"; } > "$SSHD_CONFIG" || die "не записать $SSHD_CONFIG — SSH не настроен"
fi
# Ключи хоста macOS создаёт при первом включении Remote Login; если его ни разу не включали, их нет и sshd -t падает — создаём заранее.
ssh-keygen -A </dev/null >/dev/null || die "не создать ключи хоста SSH"
sshd -t </dev/null || { rm -f "$SSHD_DROPIN"; [ "$legacy" -eq 0 ] || cp -p "$SSHD_CONFIG.dsa-backup" "$SSHD_CONFIG"; die "sshd не принял настройки — они убраны, SSH не настроен"; }

# systemsetup -setremotelogin требует Full Disk Access для Терминала, launchctl — нет.
if launchctl print system/com.openssh.sshd </dev/null >/dev/null 2>&1; then
    step "Remote Login был включён до установки — теперь по SSH пускает только по ключам и только из mesh"
else
    step "включаю вход по SSH (Remote Login)"
    launchctl load -w /System/Library/LaunchDaemons/ssh.plist </dev/null || die "не включить Remote Login"
fi
# Если в «Общий доступ → Удалённый вход» выбраны отдельные пользователи, macOS пускает только их — добавляем туда группу admin.
if dseditgroup -o read com.apple.access_ssh </dev/null >/dev/null 2>&1; then
    dseditgroup -o edit -a admin -t group com.apple.access_ssh </dev/null
fi

st=$(netbird status </dev/null 2>/dev/null)
field() { printf '%s\n' "$st" | sed -n "s/^$1: *//p" | head -n 1; }
fqdn=$(field FQDN)
nkeys=$(grep -Ec '^(ssh-|sk-ssh-|ecdsa-)' "$KEYS_FILE" 2>/dev/null)
admins=$(dscl . -read /Groups/admin GroupMembership 2>/dev/null | sed 's/^GroupMembership: *//' | tr ' ' '\n' | grep -v '^root$' | tr '\n' ' ')
if launchctl print system/com.openssh.sshd </dev/null >/dev/null 2>&1; then sshd_state='включён'; else sshd_state='ВЫКЛЮЧЕН'; fi

printf '\n--- NetBird ---\n'
printf 'Версия:       %s\n' "$(netbird version </dev/null 2>/dev/null)"
printf 'Management:   %s\n' "$(field Management)"
printf 'NB IP:        %s\n' "$(field 'NetBird IP')"
printf 'FQDN:         %s\n' "$fqdn"
printf 'Peers:        %s\n' "$(field 'Peers count')"
printf '\n--- SSH ---\n'
printf 'Remote Login: %s\n' "$sshd_state"
printf 'Ключи DSA:    %s шт., обновление раз в сутки (%s)\n' "${nkeys:-0}" "$REFRESH_LABEL"
printf 'Firewall:     %s\n' "$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null)"
printf 'Вход:         ssh <учётка>@%s — учётки группы admin: %s\n' "$fqdn" "$admins"
printf '\n=== Готово ===\n'
