Compare commits
15 commits
main
...
agent/serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7191d54a40 | ||
|
|
a0d539b1f6 | ||
|
|
ae1c0741e5 | ||
|
|
7b11ce9da2 | ||
|
|
b18d834c9f | ||
|
|
4baf8f8b3c | ||
|
|
01968d79a7 | ||
|
|
71cc35e986 | ||
|
|
78e650a35b | ||
|
|
66b9c3730e | ||
|
|
4016d0e7e7 | ||
|
|
5bce599d2f | ||
|
|
d50d28369a | ||
|
|
29afdbeb6d | ||
|
|
9289eb8a13 |
26 changed files with 1948 additions and 2306 deletions
26
.github/workflows/bash-syntax.yml
vendored
26
.github/workflows/bash-syntax.yml
vendored
|
|
@ -13,8 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
# v4.2.2 uses Node 20, supported by the current Forgejo runner.
|
||||
uses: https://code.forgejo.org/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check every installer
|
||||
run: |
|
||||
|
|
@ -48,20 +47,17 @@ jobs:
|
|||
- name: Test step 8 AI agent catalog
|
||||
run: bash tests/test-step8-agent-catalog.sh
|
||||
|
||||
- name: Test firewall and panel security
|
||||
run: bash tests/test-firewall-panel-security.sh
|
||||
- name: Test Server Monitor Manager enrollment
|
||||
run: bash tests/test-monitor-manager-enrollment.sh
|
||||
|
||||
- name: Test pinned downloads
|
||||
run: bash tests/test-download-security.sh
|
||||
- name: Test Server Monitor Manager Link policies
|
||||
run: bash tests/test-monitor-manager-links.sh
|
||||
|
||||
- name: Test notifications and backup security
|
||||
run: bash tests/test-notification-backup-security.sh
|
||||
- name: Test Server Monitor Manager lifecycle
|
||||
run: bash tests/test-monitor-manager-lifecycle.sh
|
||||
|
||||
- name: Test initial backup policy and upload retry
|
||||
run: bash tests/test-backup-initial-retry.sh
|
||||
- name: Test Server Monitor Manager extended metrics
|
||||
run: bash tests/test-monitor-manager-metrics.sh
|
||||
|
||||
- name: Test step 3 socket activation runtime directory
|
||||
run: bash tests/test-step3-runtime-directory.sh
|
||||
|
||||
- name: Test step 2 UFW boot readiness
|
||||
run: bash tests/test-step2-ufw-boot.sh
|
||||
- name: Test Server Monitor Manager control layer installer
|
||||
run: bash tests/test-monitor-manager-control-layer.sh
|
||||
|
|
|
|||
123
README.md
123
README.md
|
|
@ -8,37 +8,30 @@ A modular set of Bash installers for the initial setup, security hardening, admi
|
|||
|
||||
## Quick installation
|
||||
|
||||
Obtain an approved full commit SHA and the archive SHA-256 through a trusted release/review channel. The digest is specific to the selected source: Forgejo and GitHub archives can differ. Do not use a branch name or obtain the expected digest from the downloaded archive itself. Until a reviewed release publishes these values, this procedure requires the operator to supply them.
|
||||
|
||||
The complete block verifies the archive before starting the wizard. Forgejo is the default; GitHub is optional:
|
||||
Copy the complete block into the server terminal. It downloads the repository archive through `codeload.github.com`, stops on errors, and starts the unified installer:
|
||||
|
||||
```bash
|
||||
set -e
|
||||
cd "$HOME"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl ca-certificates tar
|
||||
read -rp 'Reviewed full commit SHA (40 hex): ' revision
|
||||
read -rp 'Trusted archive SHA-256 (64 hex): ' expected
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ && "$expected" =~ ^[0-9a-f]{64}$ ]]
|
||||
read -rp 'Source: forgejo or github [forgejo]: ' source
|
||||
case "${source:-forgejo}" in
|
||||
forgejo) url="https://git.ochenstarik.ru/ochenstarik/lightweight-server/archive/${revision}.tar.gz" ;;
|
||||
github) url="https://codeload.github.com/ochenstarik-ui/lightweight-server/tar.gz/${revision}" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
archive="$(mktemp)"
|
||||
trap 'rm -f "$archive"' EXIT
|
||||
curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 \
|
||||
--retry 3 --connect-timeout 30 --max-time 300 "$url" -o "$archive"
|
||||
printf '%s %s\n' "$expected" "$archive" | sha256sum -c -
|
||||
# Refuse to mix a reviewed archive with files from an earlier installation.
|
||||
[[ ! -e lightweight-server ]]
|
||||
mkdir -m 700 lightweight-server
|
||||
curl -4 -fL \
|
||||
--retry 5 \
|
||||
--retry-delay 10 \
|
||||
--connect-timeout 30 \
|
||||
https://codeload.github.com/ochenstarik-ui/lightweight-server/tar.gz/refs/heads/main \
|
||||
-o "$archive"
|
||||
install -d -m 700 lightweight-server
|
||||
tar -xzf "$archive" -C lightweight-server --strip-components=1
|
||||
rm -f "$archive"
|
||||
trap - EXIT
|
||||
cd lightweight-server
|
||||
chmod 700 ./*.sh
|
||||
sudo ./ochenstarik-server-install.sh
|
||||
```
|
||||
|
||||
The first wizard prompt selects the dialog language. English is the default; Russian, Spanish, German, French, Portuguese, Simplified Chinese, Japanese, Arabic, and Hindi are also available. The wizard then opens the main menu with steps 1–8. Select any step, run it, and the wizard returns to the main menu.
|
||||
|
||||
Keep the current SSH session open while changing the SSH port. Test the new login in a second terminal before disconnecting.
|
||||
|
|
@ -55,9 +48,10 @@ The wizard intentionally requires all module scripts to be present locally. Use
|
|||
| `ochenstarik-server-user-3.sh` | Administrative user, SSH key permissions, SSH migration, sudo, and fail2ban | Run after step 2 |
|
||||
| `ochenstarik-server-tg-4.sh` | Telegram notifications for successful SSH logins | Optional |
|
||||
| `ochenstarik-server-vpn-5.sh` | System VPN through Xray; repeated runs can connect, disconnect, reconfigure, or show status | Optional |
|
||||
| `ochenstarik-server-panel-warp-6.sh` | 3x-ui panel and optional local Cloudflare WARP proxy | Optional |
|
||||
| `ochenstarik-server-panel-warp-6.sh` | 3x-ui panel and local Cloudflare WARP proxy | Optional |
|
||||
| `ochenstarik-server-backup-7.sh` | Protected initial snapshot and selected daily, weekly, or monthly schedules | Optional |
|
||||
| `ochenstarik-server-ai-agents-8.sh` | Installs selected AI agents: Hermes, OpenClaw, OpenHands, OpenCode, Aider, AutoGPT, or Pi Coding Agent | Optional |
|
||||
| `ochenstarik-server-monitor-manager.sh` | Installs the Server Monitor Manager SSH endpoint, public WireGuard Hub, or outbound-only Node | Optional; Hub needs a public UDP endpoint |
|
||||
| `ochenstarik-server-uninstall.sh` | Removes project-managed settings so installation can start again | Use carefully |
|
||||
|
||||
Every module can be run independently from the full archive or cloned repository:
|
||||
|
|
@ -70,6 +64,68 @@ sudo ./SCRIPT_NAME.sh
|
|||
|
||||
Replace `SCRIPT_NAME.sh` with the required filename from the table.
|
||||
|
||||
## Server Monitor Manager Hub and Nodes
|
||||
|
||||
The Hub needs a public IPv4 address or domain and an open UDP port (default `51820`). Secondary Nodes establish outbound WireGuard connections and do not need a public IP.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-monitor-manager.sh
|
||||
chmod 700 ochenstarik-server-monitor-manager.sh
|
||||
bash -n ochenstarik-server-monitor-manager.sh
|
||||
sudo ./ochenstarik-server-monitor-manager.sh hub
|
||||
```
|
||||
|
||||
Create a separate 10-minute enrollment code for each Node on the Hub, then run the same installer with `node` on the matching server:
|
||||
|
||||
```bash
|
||||
sudo ochenstarik-smm node-code ai-agent
|
||||
sudo ochenstarik-smm node-code home
|
||||
sudo ./ochenstarik-server-monitor-manager.sh node
|
||||
```
|
||||
|
||||
Links are directional. The reverse direction requires its own rule:
|
||||
|
||||
```bash
|
||||
sudo ochenstarik-smm link-connect ai-agent home tcp 22 120
|
||||
sudo ochenstarik-smm link-disconnect ai-agent home tcp 22
|
||||
sudo ochenstarik-smm nodes
|
||||
sudo ochenstarik-smm links
|
||||
```
|
||||
|
||||
The Node installer creates its private WireGuard key locally and prints an `SMMREQ1` request. In a second terminal run `sudo ochenstarik-smm node-enroll` on the Hub, paste the request at the hidden prompt, and return the resulting `SMMACK1` code to the Node installer. The enrollment code expires after 10 minutes and is consumed by the first successful registration.
|
||||
|
||||
Role-aware maintenance commands create a root-only backup before destructive changes:
|
||||
|
||||
```bash
|
||||
sudo ./ochenstarik-server-monitor-manager.sh update
|
||||
sudo ./ochenstarik-server-monitor-manager.sh rollback
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-node
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-hub
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-monitor
|
||||
```
|
||||
|
||||
### Experimental persistent control layer
|
||||
|
||||
The first persistent Hub/Agent layer is available as an alpha for three-server testing. It uses self-contained single-file binaries, SQLite on the Hub, one-time enrollment codes, and outbound mTLS heartbeats from Nodes. The existing SSH/WireGuard layer remains installed as a fallback while this alpha is tested.
|
||||
|
||||
Install the Control service after the WireGuard Hub:
|
||||
|
||||
```bash
|
||||
sudo ./ochenstarik-server-monitor-manager.sh install-control-hub
|
||||
sudo ./ochenstarik-server-monitor-manager.sh control-code home
|
||||
sudo ./ochenstarik-server-monitor-manager.sh control-device-code windows-pc
|
||||
```
|
||||
|
||||
Copy the resulting `SMMCTL1-...` code and install the Agent on the matching Node:
|
||||
|
||||
```bash
|
||||
sudo ./ochenstarik-server-monitor-manager.sh install-control-agent
|
||||
```
|
||||
|
||||
Create a new code for every Node. The code contains the public Control CA certificate but never its private key. Release archives are verified against their published SHA-256 checksums before installation. The Hub listens on TCP `7443`; allow this port at the hosting provider firewall if an external firewall is used.
|
||||
|
||||
`control-device-code` creates a separate operator identity for the Windows application. Agent certificates cannot list all servers, change Links, or subscribe to the operator event stream. The Control service can invoke only the dedicated `link-connect` and `link-disconnect` policy wrapper through sudo; other Hub administration commands are not granted to it.
|
||||
|
||||
## Installation flow
|
||||
|
||||
1. Select the wizard dialog language.
|
||||
|
|
@ -78,7 +134,7 @@ Replace `SCRIPT_NAME.sh` with the required filename from the table.
|
|||
4. Create or update the administrative user, install the public SSH key, repair ownership and permissions, and move SSH to the port selected in step 2.
|
||||
5. Optionally configure Telegram SSH-login notifications.
|
||||
6. Optionally configure the system VPN and subscription port.
|
||||
7. Optionally deploy 3x-ui with separate panel/subscription access choices; WARP is a separate opt-in.
|
||||
7. Optionally deploy 3x-ui and Cloudflare WARP with selected panel and subscription ports.
|
||||
8. Optionally create the permanent initial backup and configure rotating schedules.
|
||||
9. Optionally install one or more AI agents for the administrative user.
|
||||
|
||||
|
|
@ -95,16 +151,8 @@ sudo ./ochenstarik-server-vpn-5.sh --status
|
|||
|
||||
Disconnecting removes the system routing rules but keeps Xray and its configuration so the VPN can be reconnected without entering the subscription again.
|
||||
|
||||
## Pinned VPN and panel installers
|
||||
|
||||
Steps 5 and 6 require the bundled `ochenstarik-server-supply-chain.sh`. They verify the installers and executable payloads against committed SHA-256 values: 3x-ui `v3.10.2`, Xray installer commit `05aa47266c94e6fb5ca48cad96afdb4becb5891f`, Xray-core `v26.10.21` (amd64/arm64). Select GitHub or an operator-prepared HTTPS Forgejo mirror. The mirror must contain the files listed below; `ochenstarik-server-supply-chain-pins.txt` summarizes the hashes enforced by the helper; no mirror release is assumed to exist. Unknown installer downloads fail closed. Ordinary OS packages still require access to configured APT repositories.
|
||||
|
||||
The mirror layout is flat: `xui-install.sh`, `x-ui-linux-amd64.tar.gz` or `x-ui-linux-arm64.tar.gz`, `x-ui.sh`, `x-ui.service.debian`, `xray-install.sh`, and `Xray-linux-64.zip` or `Xray-linux-arm64-v8a.zip`. Digests are the committed values; the helper creates digest sidecars locally. Step 5 only needs the two Xray files.
|
||||
|
||||
## Optional AI agents
|
||||
|
||||
Do not install step 8 on VPN servers or Telegram client nodes. Use a separate machine for AI agents.
|
||||
|
||||
Step 8 offers a multiple selection of seven popular agents: Hermes Agent, OpenClaw, OpenHands, OpenCode, Aider, AutoGPT, and Pi Coding Agent. Installers are downloaded only from each project's official HTTPS endpoint, checked with `bash -n`, and launched as the selected regular user rather than directly as `root`.
|
||||
|
||||
AutoGPT is a larger Docker-based platform. Its official installer can request `sudo`, install Docker-related dependencies, and start local services. Pi is a lightweight terminal coding agent; after installation, start it with `pi` and use `/login` to connect a supported provider.
|
||||
|
|
@ -117,10 +165,7 @@ The script does not request or store model-provider API keys. After installation
|
|||
- A repeated step 2 run can change the IP-family mode or add ports without reinstalling everything.
|
||||
- Step 3 reads the SSH port chosen in step 2 and creates an administrator with sudo access.
|
||||
- The home directory, `.ssh`, and `authorized_keys` ownership and permissions are checked on every run.
|
||||
- Defaults expose only the SSH ports needed during migration and TCP 443. Port 80, subscriptions (2096), WARP (40000), and 63636 are not opened automatically. Additional application rules require an explicit selection; existing managed application rules are listed for an explicit keep/remove decision.
|
||||
- After you successfully log in on the new SSH port and confirm this in step 3, it removes temporary TCP 22 access from UFW and managed state. Without confirmation it keeps 22; no cleanup runs when SSH remains on 22 or UFW management is disabled.
|
||||
- The panel defaults to `webListen=127.0.0.1`, checked against real TCP listener addresses. Reach it through an SSH tunnel. Subscriptions independently default to loopback, can be disabled, or can be explicitly published with a random secret path. Existing sufficiently random paths are preserved by default; rotation requires confirmation because it invalidates client URLs. Preserve subscription URLs privately. Only explicitly public services receive UFW rules. WARP defaults to off and remains loopback-only when selected.
|
||||
- Telegram login notifications run only for PAM `open_session`; detached delivery does not wait for Telegram during login or send a second login notification at session close.
|
||||
- Do not remove temporary port 22 access until a new SSH session succeeds on the selected port.
|
||||
|
||||
## Backups
|
||||
|
||||
|
|
@ -131,9 +176,7 @@ Step 7 can create:
|
|||
- weekly backups retained for 4 weeks;
|
||||
- monthly backups retained for 12 months.
|
||||
|
||||
Schedules are selected independently. Backups contain server secrets. Local plaintext `.tar.zst` remains the default; opt into age encryption by providing the recipient public key. Encrypted output is streamed into `.tar.zst.age`, with no temporary plaintext archive. Keep the private recovery key separately and test decryption before relying on a backup.
|
||||
|
||||
Optional SCP upload sends the completed archive and checksum with batch authentication and strict host-key checking against the operator-supplied known_hosts file. Configure a dedicated destination account and existing SSH key; no passwords or automatic host-key trust. Upload errors are failures. An existing plaintext initial snapshot is preserved: selecting age then stops with an explicit error rather than claiming it is encrypted. Move it to safe storage and resolve the initial-snapshot policy before continuing. Re-running `initial` verifies the existing checksum and retries offsite upload, including after an earlier upload failure. Local retention does not provide remote retention or protect against loss of the host. To restore an encrypted archive, decrypt with `age --decrypt -i RECOVERY_KEY` before passing the stream through `zstd -dc | tar`; never commit recovery keys.
|
||||
Schedules are selected independently, so any combination can be enabled. Keep an additional copy on another server or storage device; a local backup does not protect against loss of the entire host.
|
||||
|
||||
## Multi-server monitoring and management
|
||||
|
||||
|
|
@ -151,12 +194,18 @@ The manager is under active early development. Review its security and developme
|
|||
- Ubuntu Server with systemd and APT;
|
||||
- root access through `sudo`;
|
||||
- an active SSH session;
|
||||
- outbound HTTPS access to the selected archive/vendor source and configured APT repositories;
|
||||
- outbound HTTPS access to GitHub or GitHub codeload;
|
||||
- a valid public SSH key for the administrator setup.
|
||||
|
||||
## Update
|
||||
|
||||
Use another reviewed full commit and its source-specific archive checksum with the verified download procedure above, into a fresh directory. Review the changes before running modules again; do not update production installers by pulling a mutable branch.
|
||||
For a Git clone:
|
||||
|
||||
```bash
|
||||
cd ~/lightweight-server
|
||||
git pull
|
||||
sudo ./ochenstarik-server-install.sh
|
||||
```
|
||||
|
||||
## Reset project-managed configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
# Lightweight-server: S1–S10, 23.10.2026
|
||||
|
||||
Технический результат: **fixed**, S8 — **no_change** (защита уже была в исходном main). Релизная приёмка и merge остаются отдельным gate; автор ветку не вливает. База: `ef83559f7de19fd8649c78b22b05df397650cf46`. Ветка: `codex/lightweight-security-20261023`.
|
||||
|
||||
Задание: `agents/codex/inbox/2026-10-23-lightweight-security.md`. Изолированный worktree существующего репозитория, без clone. На хосте и реальных RU/DE/FI/DK серверах установщики и root-команды не запускались. Создана собственная одноразовая KVM Ubuntu 24.04; после проверок остановлена, её диски, seed и временные SSH-ключи удалены.
|
||||
|
||||
## Граница исправления
|
||||
|
||||
Установщики сами создавали публичные правила и wildcard-слушатели. Исправлена вся цепочка этапов 2/3/6, включая сохранённые управляемые правила и первый запуск upstream-установщика. Частичная правка одного списка портов оставляла бы прежние разрешения и другие этапы, открывающие их снова.
|
||||
|
||||
| Пункт | Изменение и подтверждение |
|
||||
| --- | --- |
|
||||
| S1 | По умолчанию только текущий/будущий SSH и TCP 443. Нет автоматического 80/2096/40000/63636. Старые управляемые правила требуют явного сохранения; при отказе удаляются реальные UFW-правила IPv4/IPv6 и лишь затем обновляется файл состояния. Проверены реальные правила в VM. |
|
||||
| S2 | `webListen=127.0.0.1`, адрес проверяется через `ss`. Перед первым upstream start/restart временная обёртка systemctl применяет loopback к панели и отключает подписки; ошибка не допускает старт. Итоговая конфигурация проверяется до снятия защиты и после запуска. |
|
||||
| S3 | Режимы панели и подписок независимы. Подписки: loopback / выключены / явно публичные со случайным путём. Прежний валидный приватный путь сохраняется; достаточно длинный секретный публичный путь сохраняется по умолчанию. Ротация запрашивает подтверждение и предупреждает о поломке старых URL. Путь в журнал не выводится. |
|
||||
| S4 | Закреплены 3x-ui v3.10.2, Xray-install `05aa47266c94e6fb5ca48cad96afdb4becb5891f`, Xray-core v26.10.21 и SHA-256 всех используемых исполняемых payload amd64/arm64. Локальный helper проверяет их перед запуском; неизвестные транзитивные загрузки 3x-ui блокируются. GitHub либо HTTPS Forgejo mirror, без fallback. В README загрузка самого проекта по полному проверенному SHA и доверенной сумме, без mutable main. |
|
||||
| S5 | После подтверждённого оператором нового входа удаляются правила 22/tcp и запись в управляемом списке. Если UFW всё ещё разрешает IPv4/IPv6 22 либо статус не проверен, скрипт не объявляет успех и сохраняет запись. Без подтверждения, при SSH=22 или MANAGE_UFW=no очистка не выполняется. |
|
||||
| S6 | Только PAM open_session. Отправка через фиксированный `/usr/bin/setsid`, отдельный процесс, stdin/stdout/stderr закрыты. Производственные пути конфигурации и журнала фиксированы, без тестовых environment overrides. Тест: close не отправляет, open запускает одну отправку, возврат <500ms при curl, задержанном на 1s. |
|
||||
| S7 | WARP — отдельный opt-in, по умолчанию выключен. |
|
||||
| S8 | Уже присутствовали `--proto '=https' --tlsv1.2`; исходный файл не изменён, регрессия закреплена новым тестом. |
|
||||
| S9 | Кавычки и обратные слэши экранируются в curl config, управляющие символы отклоняются до вызова curl. Проверены newline-инъекция и большой URL с vertical-tab, без pipefail/grep-q обхода. Секретная ссылка не передаётся в argv curl. |
|
||||
| S10 | Опциональный age encrypt streaming без временного plaintext-архива; SCP с BatchMode и строгим заранее проверенным known_hosts. Ошибка выгрузки — ошибка операции. Повтор initial проверяет checksum и повторяет выгрузку; несовместимый старый plaintext при выбранном age явно отклоняется без удаления снимка. В VM настоящий age/zstd/tar round-trip совпал с исходным fixture. |
|
||||
|
||||
Файлы: этапы `2`, `user-3`, `tg-4`, `vpn-5`, `panel-warp-6`, `backup-7`; новый `ochenstarik-server-supply-chain.sh` и справочный `...pins.txt`; README EN/RU, CI workflow и четыре новых теста. Исходные восемь тестов не изменены.
|
||||
|
||||
## Проверки по порядку
|
||||
|
||||
1. `find . -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n` — PASS.
|
||||
2. Новые поведенческие проверки:
|
||||
- `tests/test-firewall-panel-security.sh`: defaults, миграция старых UFW-правил, IPv6 residual error, sudo без SSH_CONNECTION, независимые режимы/сохранение путей, startup guard, ожидание слушателя, подтверждение удаления 22;
|
||||
- `tests/test-download-security.sh`: escaping/контрольные символы, cache shim lifecycle и запрет неизвестного installer URL, реальные вызовы helper из этапов 5/6, отсутствие upstream main, отсутствие fallback при ошибке mirror;
|
||||
- `tests/test-notification-backup-security.sh`: PAM open/close, detached delivery, строгие SSH options и ошибка offsite;
|
||||
- `tests/test-backup-initial-retry.sh`: реальный early-return путь initial, ошибка SCP → повтор, неизменность snapshot/checksum, age policy с прежним plaintext.
|
||||
3. `for test in tests/*.sh; do bash "$test"; done` — **12/12 PASS**; `git diff --check` — PASS; diff исходных `tests/` пуст.
|
||||
4. Одноразовая Ubuntu 24.04 amd64, настоящий systemd/UFW/3x-ui: этап 2 (полная установка), миграция старых правил, этап 6 fresh install и повтор с публичными подписками — exit 0.
|
||||
5. Тот же VM: helper `prepare-xray` → `install-xray` — exit 0, реальный Xray **26.10.21**; age-backup round-trip — PASS.
|
||||
|
||||
## Настоящие UFW и TCP-слушатели
|
||||
|
||||
Ubuntu image SHA-256: `04e8f2461a16a44e78a370284471f29616cdd9baf1893df25807b71f985b6c94`, проверен по официальному SHA256SUMS cloud-images. SSH в тесте оставлен на 22; WARP выключен.
|
||||
|
||||
После полного этапа 2 и после удаления прежних 80/2096/40000/63636:
|
||||
|
||||
```text
|
||||
ufw status verbose
|
||||
Status: active
|
||||
Logging: on (low)
|
||||
Default: deny (incoming), allow (outgoing), disabled (routed)
|
||||
New profiles: skip
|
||||
22/tcp ALLOW IN Anywhere
|
||||
443/tcp ALLOW IN Anywhere
|
||||
22/tcp (v6) ALLOW IN Anywhere (v6)
|
||||
443/tcp (v6) ALLOW IN Anywhere (v6)
|
||||
```
|
||||
|
||||
После fresh этапа 6 с приватной панелью и приватными подписками UFW остаётся тем же. Полный `ss -ltn` (сокращены только столбцы очередей):
|
||||
|
||||
```text
|
||||
Local Address:Port Peer Address:Port
|
||||
127.0.0.1:2096 0.0.0.0:*
|
||||
127.0.0.1:2053 0.0.0.0:*
|
||||
0.0.0.0:22 0.0.0.0:*
|
||||
127.0.0.54:53 0.0.0.0:*
|
||||
127.0.0.1:62789 0.0.0.0:*
|
||||
127.0.0.53%lo:53 0.0.0.0:*
|
||||
127.0.0.1:11111 0.0.0.0:*
|
||||
[::]:22 [::]:*
|
||||
```
|
||||
|
||||
DNS/внутренние Xray-порты остаются локальными. Разрешение UFW 443 не означает, что скрипт создаёт на 443 отдельный HTTPS-сервис. Во время sampled fresh bootstrap наблюдались только `127.0.0.1:49587` (временный upstream порт), `127.0.0.1:2053`, `127.0.0.1:2096`; sampling дополняет pre-start enforcement, не доказывает отсутствие любого мгновенного слушателя сам по себе.
|
||||
|
||||
При явном публичном доступе к подпискам UFW дополнительно содержит:
|
||||
|
||||
```text
|
||||
2096/tcp ALLOW IN Anywhere
|
||||
2096/tcp (v6) ALLOW IN Anywhere (v6)
|
||||
```
|
||||
|
||||
`ss -ltn`: панель **127.0.0.1:2053**, подписки **\*:2096**. DB: `webListen=127.0.0.1`, `subListen=''`, `subEnable=true`. Секретный путь не выгружался в журнал.
|
||||
|
||||
## Два аудитора и собственная проверка
|
||||
|
||||
Дешёвые исполнители: `lightweight_firewall_panel`, `lightweight_downloads_notifications_backup`. До патча независимый boundary investigator; после кандидата два свежих read-only Codex auditor: `lightweight_security_audit_one`, `lightweight_security_audit_two`. Один цикл аудита, без самооценки авторского кода как релизного approval.
|
||||
|
||||
Подтверждены и исправлены найденные аудиторами: сохранённые UFW entries, игнорирование ошибки удаления 22, IPv6 статус, потеря/ротация прежнего subscription path, initial/offsite retry и несовместимая age policy, неточное описание pin manifest. Root дополнительно воспроизвёл реальные ошибки: sudo очищает SSH_CONNECTION, повторный mkdir shim и readiness race после systemctl start. После исправления повторил focused, все repository checks и настоящий VM-прогон. Неподтверждённое замечание об обязательной root escalation через PAM аудитор отозвал; тестовые env overrides всё равно удалены из производства.
|
||||
|
||||
## Evidence и пределы
|
||||
|
||||
Сырые обезличенные результаты сохранены в managed Codex Security artifact storage, target `lightweight-server`, bundle `artifacts-827d7430ed6521a319c17103be0c53cc1d742842b3e097b3a5f63693cead15d9`, каталог `artifacts/lightweight-20261023/`:
|
||||
|
||||
| Файл | SHA-256 |
|
||||
| --- | --- |
|
||||
| final-checks.log | 57be6a409fbce5488611bbc853eaecaacecdde8fa7c40656b87fb2ec925970f2 |
|
||||
| ubuntu24-step2.log | 6e37718bf82ef1bf061449dee8782d79855946dca2ba5776e6bc9bada3dbec37 |
|
||||
| ubuntu24-migration.log | 0257a0c97a8dd592c3492d886ad6fb42af2fedff7139eb70ea16387a8b2e2bab |
|
||||
| ubuntu24-private.log | 4de0099004bcb79822b5a915d1e5914ea3b447e8b41e67471c730f75f39553c8 |
|
||||
| ubuntu24-public.log | f0d75dd045e89ca9f8066542e4806b20c10724165e07f56f659b35c83728274f |
|
||||
| ubuntu24-age.log | 737740b39ceba68214737f21283fea1d37a5f448c5bb3addcb6eef1d39fbcc09 |
|
||||
| ubuntu24-xray.log | 270af1adae421035c3d97d4b0779c2d2f922e72208988036a3e534cff27c2a14 |
|
||||
|
||||
Полная VPN-маршрутизация реального сервера, WARP opt-in, arm64 execution и реальный SCP на чужой хост не запускались; вне требуемого Ubuntu amd64 прогона и разрешений. Mirror проверен на fail-closed отказ; готовый публичный mirror релиз не опубликован, нужны файлы с указанными суммами. Блок README требует реально одобренные SHA+checksum и не выдумывает готовый release.
|
||||
|
||||
По последнему правилу владельца новый PR ждёт Grok на точном SHA либо Claude по прямому запросу владельца. Два технических Codex-аудита этот release gate не заменяют. CI удалённой ветки проверяется отдельно после публикации; этот отчёт фиксирует локальные/VM результаты.
|
||||
|
||||
## Поправка CI после публикации
|
||||
|
||||
Первоначальный SHA `9f06756cefb56be41fb815edcf526f2eef924ef4`, автоматические push/PR runs 1/2 (tasks 7364/7365): FAIL **до checkout**, не отказ тестов. Forgejo 9.0.3, runner v6.4.0 отклонил `runs.using=node24` существующего checkout@v6. Workflow закреплён на совместимом Node20 checkout v4.2.2, точный upstream/mirror SHA `11bd71901bbe5b1630ceea73d27597364c9af683`; `action.yml` проверен из Forgejo mirror. Все 12 проверок сохранены. Результат автоматического CI нового head фиксируется в PR и AI-Memory; повторный dispatch не выполнялся.
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
# RU/DE VPS — настройка 23.10.2026
|
||||
|
||||
**База и прямой VPN-вход из текущей сети: PASS.** Первоначальный HOLD устранён точечным исключением RU-IP из локального прозрачного прокси. Обычный REALITY-клиент без SSH выходит в зарубежный Интернет через DE, к российскому сервису через RU. Другая сеть и устройство семьи проверяются при переносе. Финляндия и Дания не затронуты.
|
||||
|
||||
## Основание и исходники
|
||||
|
||||
Прямое задание владельца: `kagent/agents/claude/done/2026-10-23-fixes/CODEX-vps-setup-ru-de.md`; повтор `kagent/kagent` в переданном пути исправлен. Только два адреса из разрешённых RU/DE файлов; иных SSH-хостов не проверял. Порядок: RU полностью, затем DE, затем связка.
|
||||
|
||||
Развёрнуты скрипты **main после PR19**, точный merge SHA `47d5e030dfa392a995649c0519088aae74f73935` (PR19 head `eb00e38a7515c6772535322d7b353c9c01718fcc`). Merge и main CI5/task7370 SUCCESS проверены до работы. Использован отдельный worktree существующего репозитория; скрипты переданы SCP, все SHA-256 сверены на серверах. С GitHub репозиторий на VPS не клонировался. Продуктовых прогонов KAgent/Telegram не запускал.
|
||||
|
||||
Первый парольный вход — один на каждый VPS через SSH_ASKPASS, без паролей в argv/history/логах/временных копиях. Далее — новый ключ оператора. Исходные `.env` и `.ppk` не копировались и не менялись, каталог700/файлы600 сохранены. У `.ppk` читались только заголовки и ровно Public-Lines, остановка до приватной части.
|
||||
|
||||
Host keys (Ed25519), зафиксированы при первом контакте, **TOFU**, не независимая внеполосная аттестация:
|
||||
|
||||
- RU: `SHA256:rYHjuLL2NjiHlImXA3ShWCeZBrdoGbGw0phQfxafJlE`
|
||||
- DE: `SHA256:UeebWqUQoNx4FePQGV4P14iSpzZHClFDjndLm/9k3DI`
|
||||
|
||||
Постоянный known_hosts: `~/.ssh/kagent_vps_known_hosts`; новый порт привязан к тому же ранее проверенному host key. Создан `~/.ssh/kagent_vps_ed25519` (0600). Приватный ключ не передавался на VPS. В новом admin authorized_keys обоих — публичные ключи владельца и оператора. Полный вход оператора и sudo с паролем проверены. Сервер отдельно подтвердил приём публичного ключа владельца (SSH public-key probe); end-to-end подпись его приватным ключом не выполнялась, поскольку приватную часть PPK читать запрещено.
|
||||
|
||||
## Выполненные этапы
|
||||
|
||||
| Проверка | RU | DE |
|
||||
| --- | --- | --- |
|
||||
| ОС | Ubuntu24.04 LTS: проверка24.04.4, после обновления24.04.5 | то же |
|
||||
| Часовой пояс | Europe/Moscow | Europe/Berlin |
|
||||
| Этап1 | swap2GiB, текущая локаль сохранена, необязательные программы пропущены | существующий swap512MiB сохранён, локаль сохранена, программы пропущены |
|
||||
| Этап2 | IPv4, выбранный SSH +443, временный22 затем удалён | то же |
|
||||
| Этап3 | новый admin, два ключа, SSH root/password/kbdinteractive no, sudo с паролем | то же |
|
||||
| Вторая независимая key-session | PASS до подтверждения удаления22 | PASS до подтверждения удаления22 |
|
||||
| Этап6 | 3x-ui v3.10.2; панель и подписки loopback; WARP off | то же |
|
||||
| Получение 3x-ui | GitHub, закреплённые SHA-256 main-helper | то же |
|
||||
| Страница входа через SSH-L | HTTP200 | HTTP200 |
|
||||
| Этап7 | initial snapshot +checksum PASS | initial snapshot +checksum PASS |
|
||||
| Состояние после связки и финальной смены секретов | consistent SQLite checkpoint +ручные daily snapshots/checksum PASS | то же |
|
||||
|
||||
Автоматические расписания не включались; первичные снимки не перезаписывались. После API-ротации логина/пароля/секретного пути выполнен финальный согласованный checkpoint БД и новый ручной снимок, чтобы восстановление соответствовало актуальным env-файлам. Настройка выполнялась без изменения глобальной сети/брандмауэра этой машины. Этапы4,5,8 не выполнялись. Telegram-узел DE не ставился. WARP не установлен.
|
||||
|
||||
На Ubuntu с socket activation после обновления пакетов отсутствовал `/run/sshd`; standalone `sshd -t` завершился ошибкой до рестарта. Обратимо создан стандартный служебный каталог root:root755, затем неизменённый main-скрипт прошёл проверку и перенос SSH. Текущая административная сессия сохранялась до новой key-session и password-sudo проверки.
|
||||
|
||||
Перед контрольной перезагрузкой обнаружено: **RU ufw.service была masked**, хотя действующие kernel rules были активны. До перезагрузки сохранена исходная mask-ссылка в закрытом root-каталоге `codex-ufw-boot-before-*`, выполнены `systemctl unmask ufw.service` и `systemctl enable ufw.service`. DE служба уже была enabled. Это локализованное операционное исправление, не автоматическое снятие произвольных масок установщиком.
|
||||
|
||||
**Оба VPS последовательно перезагружены.** Boot ID изменился; новая независимая key-session admin, UFW, x-ui и fail2ban после загрузки PASS. Итоговые ядра: RU6.8.0-100-generic, DE6.8.0-147-generic; ниже приведены реальные post-boot снимки. Обе страницы панелей и оба egress-направления повторно проверены после загрузки.
|
||||
|
||||
Оба дефекта оформлены **отдельным [code PR20](https://git.ochenstarik.ru/ochenstarik/lightweight-server/pulls/20)**: head `e5b06793ec69dcbdf402b72b2e661453a7732569`; 14тестов PASS, pushCI9/task7381 и PRCI10/task7382 SUCCESS именно на этом SHA. Этап3 подготавливает runtime directory, этап2 требует автозапуск UFW до изменения правил и отказывает при маске без её автоматического снятия. Существующие тесты не изменены. Полный pushlog23954B без усечения проверен, оба новых теста и Job succeeded присутствуют; SHA256 `f74aee3ac1f4d4902145a6a42554073156ccf8bd40958c43d1c7f47fd8a25483`. На VPS выполнялись описанные операционные исправления; несмерженный код PR20 не разворачивался.
|
||||
|
||||
## Связка и сетевые проверки
|
||||
|
||||
Через аутентифицированный API 3x-ui по SSH-туннелям, с штатным CSRF-token/session cookie:
|
||||
|
||||
- DE: VLESS+Reality TCP на24443, inbound `codex-ru-de-link`, клиент `ru-link`; UFW разрешает этот порт **только с RU_IP**. В общий список глобально публикуемых managed ports он не добавлен, чтобы последующий рендер не расширил CIDR-правило до Anywhere.
|
||||
- RU: VLESS+Reality443, один тестовый клиент `owner-test`, inbound `family-owner-test`; outbound `vpn-de` — в DE. Private IP и geoip:ru → direct; geosite:category-ru и домены ru/su/рф → direct; остальное по умолчанию → DE. Служебные API/stats и прежние правила сохранены.
|
||||
- Native Xray из закреплённого 3x-ui:26.10.10 на обоих; временный клиент26.10.21 (SHA-256 проверен по pinned архиву). Также проверялся клиент26.10.10: наблюдение не является только различием версий.
|
||||
- Начальный camouflage target RU Microsoft сменён на доступный Cloudflare; ключи/UUID не ротировались. DE target Microsoft. После изменения выполнен штатный reload Xray. Настройки в реальном config.json сверены с API-моделью. Подмены TLS/отключения проверок сертификата нет.
|
||||
|
||||
| Путь клиента | Зарубежный запрос api.ipify.org | Российский запрос к сервису Яндекса | Статус |
|
||||
| --- | --- | --- | --- |
|
||||
| Эта машина → публичный RU443, до исправления | TLS error35 | TLS error35 | исторический HOLD, устранён |
|
||||
| Эта машина → публичный RU443, после исправления | фактический IP=DE_IP | фактический IP=RU_IP | PASS, без SSH |
|
||||
| Эта машина → SSH-L к тому же RU443 → REALITY | фактический IP=DE_IP | фактический IP=RU_IP | PASS, это SSH-путь |
|
||||
| Временный клиент на DE → публичный RU443 → DE | фактический IP=DE_IP | фактический IP=RU_IP | PASS, внешний контроль публичного входа |
|
||||
|
||||
Российская проверка: `https://ipv4-internet.yandex.net/api/v0/ip`; ответ сопоставлен с RU_IP в памяти, сам IP не журналировался. 2ip.ru вернул HTTP-ошибку и не использовался как положительное доказательство.
|
||||
|
||||
Первоначально REALITY получал маскирующий сертификат, хотя UUID, SNI, short ID, private/public pair и загруженные настройки совпадали. Через SSH и с DE ошибки не было. Перебор fingerprint/SNI/версии не устранил исходный отказ; это историческое наблюдение.
|
||||
|
||||
### Прямой HOLD устранён — дополнительное поручение владельца
|
||||
|
||||
На этой машине активен `xray-transparent.service`: `/usr/local/sbin/xray-transparent-up` перенаправляет TCP вне `bypass4` на локальный Xray12345. RU-IP отсутствовал в исключениях. `ip route get` показывал физический интерфейс, но не последующий NAT redirect. До исправления пять отправленных ClientHello не дали ни одного пакета в ограниченном по времени захвате входящего RU443; публичный RU-IP действительно назначен интерфейсу. Часы совпадают, min/maxClientVer/maxTimeDiff не заданы. Причина HOLD — локальный прозрачный перехват; внешний DPI/MITM не заявляется.
|
||||
|
||||
После аутентификации владельца штатным `pkexec` добавлен **только RU-IP** в действующий nft set `inet xray_transparent bypass4` и тот же список startup script. Перед изменением исходный root-owned script и JSON активного set сохранены в локальном `/root/codex-ru-direct-before-*` (0700). Проверены IPv4, права, сохранение прежних CIDR/redirect, идемпотентность формирования патча; `bash -n` и `nft -c` PASS. Запись атомарная, при её неудаче предусмотрен rollback добавленного элемента. Общий прокси не останавливался и не перезапускался; RU/DE конфигурации, ключи и тестовый клиент не менялись. FI/DK не затронуты.
|
||||
|
||||
Временный клиент Xray26.10.10 подключался к публичному RU443 обычным VLESS+Reality. Proxy env удалены у клиента и curl; SSH/dialerProxy в клиентском data path отсутствуют. SSH использован только для административного захвата на RU, без forwarding443. Оба запроса сначала PASS, затем отдельный свежий клиент повторил оба PASS: `api.ipify.org` видит DE_IP, API Яндекса RU_IP, curl exit0. На RU совпали **2 из2 префиксов ClientHello** — SHA-256 первых76байт отправленного/принятого handshake; fallback «received real certificate» отсутствует. Прямой вход из текущей сети принят. Персистентный script после записи прошёл `bash -n`; перезагрузка этой машины/перезапуск общего прокси ради проверки не выполнялись.
|
||||
|
||||
Тестовая ссылка только `/home/ochenstarik/srgfd/vpn_test_link.txt` (0600), не ротировалась. При переносе проверить фактическое устройство/сеть семьи; до переноса FI не переустанавливать.
|
||||
|
||||
|
||||
## Доступ к панелям
|
||||
|
||||
Актуальные значения панели сохранены в защищённых `panel_ru.env` и `panel_de.env`; в report/Git/вывод команд их нет. Одноразовые параметры upstream-установщика, который внутри использует CLI-setting, аннулированы: логин/пароль и секретный path сменены штатным API без секретов в argv. Новый вход проверен. Проверка journal показала отсутствие актуального пароля и секретного path. Сам идентификатор пользователя 3x-ui записывает в штатный authentication audit; утверждать, что username нигде на VPS не встречается, нельзя. Аудит не отключался и не удалялся. Полный набор реквизитов сохраняется только в указанных env-файлах. Подробные готовые команды с приватными адресами/именем/SSH-портом и PuTTY-параметрами — `/home/ochenstarik/srgfd/PANEL_HOWTO.txt` (0600). SSH alias config — `/home/ochenstarik/srgfd/ssh_config` (0600).
|
||||
|
||||
Готовые команды из этой машины без публикации приватных адресов и значений из env:
|
||||
|
||||
```bash
|
||||
# RU — оставить открытым в отдельном терминале:
|
||||
PANEL_PORT="$(python3 -c 'from pathlib import Path; import shlex; print(dict((k,shlex.split(v)[0]) for k,v in (s.split("=",1) for s in Path("/home/ochenstarik/srgfd/panel_ru.env").read_text().splitlines()))["port"])')"
|
||||
ssh -F /home/ochenstarik/srgfd/ssh_config -i ~/.ssh/kagent_vps_ed25519 -N -L "127.0.0.1:18053:127.0.0.1:$PANEL_PORT" ru-vpn
|
||||
# DE — в другом терминале:
|
||||
PANEL_PORT="$(python3 -c 'from pathlib import Path; import shlex; print(dict((k,shlex.split(v)[0]) for k,v in (s.split("=",1) for s in Path("/home/ochenstarik/srgfd/panel_de.env").read_text().splitlines()))["port"])')"
|
||||
ssh -F /home/ochenstarik/srgfd/ssh_config -i ~/.ssh/kagent_vps_ed25519 -N -L "127.0.0.1:18054:127.0.0.1:$PANEL_PORT" de-vpn
|
||||
```
|
||||
|
||||
PuTTY: SSH Auth → указать существующий `.ppk` владельца; Connection → SSH → Tunnels: source18053(RU)/18054(DE), destination `127.0.0.1:<PANEL_PORT из panel_*.env>`, Local, Add. Адрес браузера: `http://127.0.0.1:18053<path из panel_ru.env>` либо `http://127.0.0.1:18054<path из panel_de.env>`. В HOWTO есть команда открытия URL через чтение env без печати секретного пути. Обе страницы входа реально проверены, не только описаны.
|
||||
|
||||
## Снимки приёмки (значения доступов и panel port отредактированы)
|
||||
|
||||
### RU
|
||||
|
||||
`os_kernel`, exit=0
|
||||
|
||||
```text
|
||||
Operating System: Ubuntu 24.04.5 LTS
|
||||
Kernel: Linux 6.8.0-100-generic
|
||||
```
|
||||
|
||||
`timezone`, exit=0
|
||||
|
||||
```text
|
||||
Europe/Moscow
|
||||
```
|
||||
|
||||
`swap`, exit=0
|
||||
|
||||
```text
|
||||
NAME TYPE SIZE USED PRIO
|
||||
/swapfile file 2G 0B -2
|
||||
```
|
||||
|
||||
`sshd`, exit=0
|
||||
|
||||
```text
|
||||
port <SSH_PORT>
|
||||
permitrootlogin no
|
||||
passwordauthentication no
|
||||
kbdinteractiveauthentication no
|
||||
```
|
||||
|
||||
`ufw`, exit=0
|
||||
|
||||
```text
|
||||
Status: active
|
||||
Logging: on (low)
|
||||
Default: deny (incoming), allow (outgoing), disabled (routed)
|
||||
New profiles: skip
|
||||
|
||||
To Action From
|
||||
-- ------ ----
|
||||
<SSH_PORT>/tcp ALLOW IN Anywhere
|
||||
443/tcp ALLOW IN Anywhere
|
||||
```
|
||||
|
||||
`ss`, exit=0
|
||||
|
||||
```text
|
||||
127.0.0.53%lo:53 users:(("systemd-resolve",pid=500,fd=15))
|
||||
127.0.0.1:2096 users:(("x-ui",pid=746,fd=19))
|
||||
127.0.0.54:53 users:(("systemd-resolve",pid=500,fd=17))
|
||||
127.0.0.1:<PANEL_PORT> users:(("x-ui",pid=746,fd=10))
|
||||
0.0.0.0:<SSH_PORT> users:(("sshd",pid=754,fd=3),("systemd",pid=1,fd=90))
|
||||
127.0.0.1:11111 users:(("xray-linux-amd6",pid=906,fd=7))
|
||||
127.0.0.1:62789 users:(("xray-linux-amd6",pid=906,fd=3))
|
||||
[::]:<SSH_PORT> users:(("sshd",pid=754,fd=4),("systemd",pid=1,fd=91))
|
||||
*:443 users:(("xray-linux-amd6",pid=906,fd=6))
|
||||
```
|
||||
|
||||
`nft_tables`, exit=0
|
||||
|
||||
```text
|
||||
table ip filter
|
||||
table ip6 filter
|
||||
```
|
||||
|
||||
`services`, exit=0
|
||||
|
||||
```text
|
||||
active
|
||||
active
|
||||
```
|
||||
|
||||
Panel DB (без path/credentials):
|
||||
|
||||
```json
|
||||
{
|
||||
"webListen": "127.0.0.1",
|
||||
"webPort": "<PANEL_PORT>",
|
||||
"subListen": "127.0.0.1",
|
||||
"subPort": "2096",
|
||||
"subEnable": "true"
|
||||
}
|
||||
```
|
||||
|
||||
### DE
|
||||
|
||||
`os_kernel`, exit=0
|
||||
|
||||
```text
|
||||
Operating System: Ubuntu 24.04.5 LTS
|
||||
Kernel: Linux 6.8.0-147-generic
|
||||
```
|
||||
|
||||
`timezone`, exit=0
|
||||
|
||||
```text
|
||||
Europe/Berlin
|
||||
```
|
||||
|
||||
`swap`, exit=0
|
||||
|
||||
```text
|
||||
NAME TYPE SIZE USED PRIO
|
||||
/swapfile file 512M 0B -2
|
||||
```
|
||||
|
||||
`sshd`, exit=0
|
||||
|
||||
```text
|
||||
port <SSH_PORT>
|
||||
permitrootlogin no
|
||||
passwordauthentication no
|
||||
kbdinteractiveauthentication no
|
||||
```
|
||||
|
||||
`ufw`, exit=0
|
||||
|
||||
```text
|
||||
Status: active
|
||||
Logging: on (low)
|
||||
Default: deny (incoming), allow (outgoing), disabled (routed)
|
||||
New profiles: skip
|
||||
|
||||
To Action From
|
||||
-- ------ ----
|
||||
<SSH_PORT>/tcp ALLOW IN Anywhere
|
||||
443/tcp ALLOW IN Anywhere
|
||||
24443/tcp ALLOW IN <RU_IP> # RU-DE Reality upstream
|
||||
```
|
||||
|
||||
`ss`, exit=0
|
||||
|
||||
```text
|
||||
127.0.0.1:62789 users:(("xray-linux-amd6",pid=1023,fd=3))
|
||||
0.0.0.0:<SSH_PORT> users:(("sshd",pid=792,fd=3),("systemd",pid=1,fd=87))
|
||||
127.0.0.1:<PANEL_PORT> users:(("x-ui",pid=788,fd=10))
|
||||
127.0.0.1:2096 users:(("x-ui",pid=788,fd=18))
|
||||
127.0.0.54:53 users:(("systemd-resolve",pid=746,fd=17))
|
||||
127.0.0.1:11111 users:(("xray-linux-amd6",pid=1023,fd=7))
|
||||
127.0.0.53%lo:53 users:(("systemd-resolve",pid=746,fd=15))
|
||||
[::]:<SSH_PORT> users:(("sshd",pid=792,fd=4),("systemd",pid=1,fd=88))
|
||||
*:24443 users:(("xray-linux-amd6",pid=1023,fd=6))
|
||||
```
|
||||
|
||||
`nft_tables`, exit=0
|
||||
|
||||
```text
|
||||
table ip filter
|
||||
table ip6 filter
|
||||
```
|
||||
|
||||
`services`, exit=0
|
||||
|
||||
```text
|
||||
active
|
||||
active
|
||||
```
|
||||
|
||||
Panel DB (без path/credentials):
|
||||
|
||||
```json
|
||||
{
|
||||
"webListen": "127.0.0.1",
|
||||
"webPort": "<PANEL_PORT>",
|
||||
"subListen": "127.0.0.1",
|
||||
"subPort": "2096",
|
||||
"subEnable": "true"
|
||||
}
|
||||
```
|
||||
|
||||
## Границы результата и следующая приёмка
|
||||
|
||||
- Прямой публичный вход из текущей сети и оба egress-направления PASS после локального исключения RU-IP. Проверка другой сети/устройства семьи и полный вход владельца своим приватным PuTTY-ключом остаются отдельными действиями.
|
||||
- Публичная панель, публичные подписки, Telegram-ноды, AI-агенты и WARP не включались. Новых дополнительных мониторов/автоматизаций нет; прежний тик30 не возобновлялся.
|
||||
- Для послеустановочного восстановления оставлены согласованные SQLite checkpoints и локальные initial/daily snapshots с проверенными checksum. Локальная копия не заменяет отдельное внешнее хранилище; offsite не включался.
|
||||
- Этот PR содержит только данный обезличенный отчёт. Code PR20 отдельный, автор не вливает. Прежний release gate для новых PR/документов без CI не обходится.
|
||||
|
||||
|
||||
## Дополнение 23.10.2026 — DE443 для домашней машины
|
||||
|
||||
Основание: актуальная заменяющая версия `kagent/agents/claude/done/2026-10-23-fixes/CODEX-vps-de-close-443.md`. Закрытие443 отменено. Эта работа — отдельный report-only PR поверх main `6f699607b079a57ea19765b19f781ef012d7de18`, где PR20 и PR21 уже merged внешним участником. Итоговый main CI18/task7390 SUCCESS; промежуточный CI17/task7389 cancelled, зелёным не объявляется.
|
||||
|
||||
SSH использован **только к de-vpn** с ранее разрешённым операторским ключом и pinned known_hosts. RU/FI/DK по SSH не посещались, их конфигурации не менялись. Системный прокси/bypass этой машины не менялся; ключ владельца/исходные доступы не копировались и не читались дополнительно.
|
||||
|
||||
Через API приватной панели DE добавлен отдельный inbound `home-server-de`: VLESS+Reality TCP443, клиент `home-server`, Vision, самостоятельные private/public key и shortId, отличные от связки24443. Camouflage `www.microsoft.com:443`: TLS1.3 и проверка цепочки с DE PASS до создания. Ссылка записана **только** в `/home/ochenstarik/srgfd/home_server_de_link.txt` (0600), без вывода URI/UUID/ключей.
|
||||
|
||||
UFW443/tcp уже открыт для всех; правило и запись443 в managed ports оставлены. UFW user.rules и managed-ports файл побайтно не изменились. Профиль `codex-ru-de-link`24443, его разрешение только с RU_IP, outbounds и routing DE не менялись: сравнены API-настройки и native existing-inbound до/после. Новый вход добавлен горячим API без смены PID Xray и без ручного рестарта.
|
||||
|
||||
Перед изменением сохранены согласованная SQLite backup, native-config и UFW/managed файлы в закрытом root checkpoint `codex-before-de-home-*`. После — новая согласованная SQLite backup с `integrity_check=ok`, обе записи443/24443 на месте, и snapshot собранного панелью конфига (`codex-after-de-home-*`). API `server/getConfigJson` содержит оба inbound. Последний записанный native config.json на диске при hot-add ещё не содержит443; он не переписывался вручную. Рабочий443 подтверждён слушателем и реальными запросами, сохранение — БД и собранным конфигом; перезапуск DE ради этого теста не выполнялся.
|
||||
|
||||
| Проверка | Результат и предел |
|
||||
| --- | --- |
|
||||
| Новый home-server, временный Xray на DE → публичный DE443 | PASS: api.ipify.org и icanhazip.com, curl0, фактический egress DE_IP |
|
||||
| Новый home-server, прямой клиент с домашней машины | Не выполнялся: DE_IP отсутствует в локальном bypass4. Самостоятельное добавление прямо запрещено этим заданием; проверка передана в отдельный `CODEX-home-xray-via-ru.md` |
|
||||
| Прежняя связка RU→DE, обычный временный клиент на этой машине | PASS: api.ipify.org egress DE_IP, российский API Яндекса egress RU_IP, оба curl0. Только клиентский трафик по прежней ссылке, без SSH/изменений RU |
|
||||
| Сохранность24443 | API/native profile, routing/outbounds и firewall неизменны; слушатель24443 активен |
|
||||
|
||||
Временные клиенты работали на свободных loopback SOCKS-портах, proxy env удалены; SSH data forwarding не использовался. Self-test DE не подменяет приёмку внешнего домашнего пути. На домашнем системном прокси новая ссылка не установлена — это задача другого поручения при владельце.
|
||||
|
||||
### DE: фактический вывод после добавления (доступы отредактированы)
|
||||
|
||||
`ufw status verbose`:
|
||||
|
||||
```text
|
||||
Status: active
|
||||
Logging: on (low)
|
||||
Default: deny (incoming), allow (outgoing), disabled (routed)
|
||||
New profiles: skip
|
||||
|
||||
To Action From
|
||||
-- ------ ----
|
||||
<SSH_PORT>/tcp ALLOW IN Anywhere
|
||||
443/tcp ALLOW IN Anywhere
|
||||
24443/tcp ALLOW IN <RU_IP> # RU-DE Reality upstream
|
||||
```
|
||||
|
||||
`ss -ltnpH` (расширенный ss -ltn, для проверки процесса):
|
||||
|
||||
```text
|
||||
LISTEN 0 4096 127.0.0.1:62789 0.0.0.0:* users:(("xray-linux-amd6",pid=1023,fd=3))
|
||||
LISTEN 0 4096 0.0.0.0:<SSH_PORT> 0.0.0.0:* users:(("sshd",pid=792,fd=3),("systemd",pid=1,fd=153))
|
||||
LISTEN 0 4096 127.0.0.1:<PANEL_PORT> 0.0.0.0:* users:(("x-ui",pid=788,fd=10))
|
||||
LISTEN 0 4096 127.0.0.1:2096 0.0.0.0:* users:(("x-ui",pid=788,fd=18))
|
||||
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=746,fd=17))
|
||||
LISTEN 0 4096 127.0.0.1:11111 0.0.0.0:* users:(("xray-linux-amd6",pid=1023,fd=7))
|
||||
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=746,fd=15))
|
||||
LISTEN 0 4096 [::]:<SSH_PORT> [::]:* users:(("sshd",pid=792,fd=4),("systemd",pid=1,fd=154))
|
||||
LISTEN 0 4096 *:443 *:* users:(("xray-linux-amd6",pid=1023,fd=9))
|
||||
LISTEN 0 4096 *:24443 *:* users:(("xray-linux-amd6",pid=1023,fd=6))
|
||||
```
|
||||
|
||||
Панель/подписки по-прежнему loopback. Новый report-only PR не содержит кода, конфигураций с ключами или самой ссылки; автоматический CI и действующий release-review gate сохраняются. Самослияние не выполняется.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Codex — lightweight-server: исправления по аудиту безопасности (23.10)
|
||||
|
||||
От: Claude, передаёт владелец. Репозиторий `git.ochenstarik.ru/ochenstarik/lightweight-server`, main `ef83559`.
|
||||
Если у тебя уже есть задание владельца по этому репо — это дополнение к нему: не дублируй, сведи в одну ветку или согласуй порядок.
|
||||
|
||||
## Правила
|
||||
- worktree от `/srv/projects/Agent projects/lightweight-server`, без клонов; одна задача — одна ветка — один PR; в main сам не вливаешь;
|
||||
- существующие тесты в `tests/` не менять, проверки не ослаблять; новые тесты — на поведение из таблицы;
|
||||
- **ничего не запускать на реальных серверах** (RU/DE/FI/DK) и на этой машине от root: проверки — `bash -n`, тесты, при необходимости
|
||||
одноразовый контейнер/VM Ubuntu 24.04 с удалением после прогона;
|
||||
- секретов в Git и логах нет; README.md и `docs/readme/README.ru.md` обновить под изменённое поведение (остальные переводы — по возможности);
|
||||
- режим: дешёвые модели → два аудитора → сам проверяешь. Отчёт — `agents/codex/done/` в этом репо.
|
||||
|
||||
## Зачем
|
||||
Серверы: RU — вход семейного VPN (3x-ui, панель только через SSH-туннель, подписки — семье), DE/FI/DK — выходы VPN и одновременно
|
||||
узлы Telegram-клиента. План: `kagent/agents/claude/done/2026-10-23-vps-plan.md`. Владелец ставит серверы этими скриптами —
|
||||
исправления нужны до российского сервера, в первую очередь S1–S3.
|
||||
|
||||
| # | Находка | Где | Серьёзность | Исправление |
|
||||
|---|---|---|---|---|
|
||||
| S1 | Шаг 2 по умолчанию открывает всему миру 22, 80, **2096**, 443, 40000, 63636. 2096 — порт подписок 3x-ui по умолчанию, поэтому «приватный режим» шага 6 не закрывает подписки; 40000/63636 открыты без сервиса | `server-2.sh:138`, `user-3.sh:491` | высокая | по умолчанию только SSH-порт и 443; остальное — явным выбором с объяснением |
|
||||
| S2 | Приватный режим панели — только правило UFW, панель слушает `0.0.0.0`. Docker и чужие nftables-таблицы (узел Telegram) обходят/перекрывают UFW | `panel-warp-6.sh` `configure_xui_ports` | высокая | в приватном режиме `webListen=127.0.0.1`; проверка `ss -ltn`, что панель не на внешнем адресе |
|
||||
| S3 | Панель и подписки — один переключатель | `panel-warp-6.sh:115` | средняя | раздельно: панель (по умолчанию закрыта, 127.0.0.1) и подписки (по выбору публично, случайный путь) |
|
||||
| S4 | 3x-ui и Xray ставятся установщиками с `main` апстрима, без версии и контрольной суммы; `bash -n` — только синтаксис. Быстрая установка самого проекта — с GitHub `main` | `panel-warp-6.sh:4`, `vpn-5.sh:404`, README | средняя | фиксированный тег релиза + sha256; источник по выбору — Forgejo `git.ochenstarik.ru` (с RU-серверов GitHub бывает недоступен) |
|
||||
| S5 | Порт 22 остаётся открытым после переноса SSH — только напечатанная подсказка | `user-3.sh:537` | средняя | после успешного входа на новый порт (подтверждение оператора) удалить правило 22 автоматически |
|
||||
| S6 | Уведомление о входе: `pam_exec` в `session` срабатывает и при открытии, и при закрытии сессии (двойное «успешный вход»); отправка синхронная, до 10 с задержки входа, если api.telegram.org недоступен (RU) | `tg-4.sh` hook | низкая | выходить, если `PAM_TYPE != open_session`; отправлять в фоне (`setsid … &`) |
|
||||
| S7 | WARP в шаге 6 обязателен; на RU не нужен и может не подняться — скрипт падает после установки 3x-ui | `panel-warp-6.sh` | низкая | WARP — отдельный вопрос/флаг |
|
||||
| S8 | Загрузка установщиков ИИ-агентов без `--proto '=https'` (редирект может уйти на http) | `ai-agents-8.sh:158` | низкая | добавить `--proto '=https' --tlsv1.2` |
|
||||
| S9 | URL подписки вставляется в конфиг curl через `printf 'url = "%s"'`: кавычка в ссылке ломает конфиг (ввод оператора) | `vpn-5.sh` | низкая | экранировать `\` и `"` или передавать URL через `--url` из файла |
|
||||
| S10 | Бэкап — полный снимок ФС с секретами (БД 3x-ui, ключи хоста, токен бота), без шифрования, только локально | `backup-7.sh` | инфо | опция шифрования (age/gpg) и выгрузки на другой хост |
|
||||
|
||||
Рекомендация к эксплуатации (не код): на серверах VPN/узлов Telegram **не ставить шаг 8 (ИИ-агенты)** — это лишний код с правами пользователя-админа.
|
||||
**Приёмка:** новые тесты в `tests/` на S1–S3, S5, S6 (рендер правил UFW/настроек 3x-ui, проверка PAM_TYPE); существующие тесты не менять; `bash -n` и все тесты PASS.
|
||||
|
||||
Дополнительно к приёмке: прогон шагов 2 и 6 в одноразовом Ubuntu 24.04 (контейнер с systemd или VM) — вывод `ufw status verbose`
|
||||
и `ss -ltn` в отчёте: открыты только SSH-порт и 443 (+ подписки, если выбраны), панель слушает только `127.0.0.1`.
|
||||
|
|
@ -8,37 +8,30 @@
|
|||
|
||||
## Быстрая установка
|
||||
|
||||
Получите полный SHA одобренного коммита и SHA-256 архива из доверенного канала релиза или ревью. Контрольная сумма зависит от источника: архивы Forgejo и GitHub могут различаться. Не используйте имя ветки и не вычисляйте «ожидаемую» сумму из самого скачанного архива. Пока проверенный релиз не опубликовал эти значения, оператор должен указать их сам.
|
||||
|
||||
Блок проверяет архив до запуска мастера. По умолчанию выбран Forgejo; GitHub можно выбрать отдельно:
|
||||
Скопируйте весь блок в терминал сервера. Он загружает архив через `codeload.github.com`, проверяет ошибки на каждом шаге и запускает единый пошаговый мастер:
|
||||
|
||||
```bash
|
||||
set -e
|
||||
cd "$HOME"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl ca-certificates tar
|
||||
read -rp 'Reviewed full commit SHA (40 hex): ' revision
|
||||
read -rp 'Trusted archive SHA-256 (64 hex): ' expected
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ && "$expected" =~ ^[0-9a-f]{64}$ ]]
|
||||
read -rp 'Source: forgejo or github [forgejo]: ' source
|
||||
case "${source:-forgejo}" in
|
||||
forgejo) url="https://git.ochenstarik.ru/ochenstarik/lightweight-server/archive/${revision}.tar.gz" ;;
|
||||
github) url="https://codeload.github.com/ochenstarik-ui/lightweight-server/tar.gz/${revision}" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
archive="$(mktemp)"
|
||||
trap 'rm -f "$archive"' EXIT
|
||||
curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 \
|
||||
--retry 3 --connect-timeout 30 --max-time 300 "$url" -o "$archive"
|
||||
printf '%s %s\n' "$expected" "$archive" | sha256sum -c -
|
||||
# Refuse to mix a reviewed archive with files from an earlier installation.
|
||||
[[ ! -e lightweight-server ]]
|
||||
mkdir -m 700 lightweight-server
|
||||
curl -4 -fL \
|
||||
--retry 5 \
|
||||
--retry-delay 10 \
|
||||
--connect-timeout 30 \
|
||||
https://codeload.github.com/ochenstarik-ui/lightweight-server/tar.gz/refs/heads/main \
|
||||
-o "$archive"
|
||||
install -d -m 700 lightweight-server
|
||||
tar -xzf "$archive" -C lightweight-server --strip-components=1
|
||||
rm -f "$archive"
|
||||
trap - EXIT
|
||||
cd lightweight-server
|
||||
chmod 700 ./*.sh
|
||||
sudo ./ochenstarik-server-install.sh
|
||||
```
|
||||
|
||||
Первым запросом мастер предлагает выбрать язык диалога и только затем открывает главное меню со всеми этапами 1–8. Выберите нужный этап, установите его, и после завершения мастер вернётся обратно в главное меню. В первом этапе пустой ввод устанавливает все рекомендуемые наборы программ. Во время изменения SSH-порта не закрывайте текущую сессию — сначала проверьте новый вход во втором терминале.
|
||||
|
||||
Мастер намеренно требует, чтобы все скрипты этапов уже лежали рядом локально. Используйте полный архив или `git clone`; одиночный файл мастера больше не подкачивает недостающие этапы из изменяемой ветки `main`.
|
||||
|
|
@ -56,10 +49,51 @@ sudo ./ochenstarik-server-install.sh
|
|||
| `ochenstarik-server-panel-warp-6.sh` | Устанавливает 3x-ui и Cloudflare WARP, настраивает и проверяет их порты | Ubuntu 22.04/24.04/26.04 или Debian 12/13 |
|
||||
| `ochenstarik-server-backup-7.sh` | Создаёт неизменяемый первичный снимок и настраивает выбранные расписания резервного копирования | Да; зависимости устанавливаются автоматически |
|
||||
| `ochenstarik-server-ai-agents-8.sh` | Устанавливает выбранных AI-агентов: Hermes, OpenClaw, OpenHands, OpenCode, Aider, AutoGPT или Pi Coding Agent | Да; API-ключи настраиваются отдельно |
|
||||
| `ochenstarik-server-monitor-manager.sh` | Устанавливает SSH endpoint, главный WireGuard Hub или подключаемый Node для Server Monitor Manager | Да; Hub требуется публичный UDP-адрес |
|
||||
| `ochenstarik-server-uninstall.sh` | Удаляет настройки, службы и данные проекта для повторной установки с начала | Да; запускать только из сохранённой SSH-сессии |
|
||||
|
||||
Файлы `*.env.example` являются только примерами. Не записывайте настоящие пароли, токены и приватные SSH-ключи в Git.
|
||||
|
||||
## Server Monitor Manager: Hub и вторичные серверы
|
||||
|
||||
Главному Hub нужен белый IPv4 или домен и открытый UDP-порт (по умолчанию `51820`). Вторичные Node подключаются исходящим WireGuard-соединением и не требуют белого IP.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-monitor-manager.sh
|
||||
chmod 700 ochenstarik-server-monitor-manager.sh
|
||||
bash -n ochenstarik-server-monitor-manager.sh
|
||||
sudo ./ochenstarik-server-monitor-manager.sh hub
|
||||
```
|
||||
|
||||
На Hub создайте отдельный enrollment-код на 10 минут для каждого Node, после чего на соответствующем сервере запустите тот же файл в режиме `node`:
|
||||
|
||||
```bash
|
||||
sudo ochenstarik-smm node-code ai-agent
|
||||
sudo ochenstarik-smm node-code home
|
||||
sudo ./ochenstarik-server-monitor-manager.sh node
|
||||
```
|
||||
|
||||
Links являются направленными:
|
||||
|
||||
```bash
|
||||
sudo ochenstarik-smm link-connect ai-agent home tcp 22 120
|
||||
sudo ochenstarik-smm link-disconnect ai-agent home tcp 22
|
||||
sudo ochenstarik-smm nodes
|
||||
sudo ochenstarik-smm links
|
||||
```
|
||||
|
||||
Установщик Node создаст приватный WireGuard-ключ локально и покажет запрос `SMMREQ1`. Во втором терминале выполните на Hub `sudo ochenstarik-smm node-enroll`, вставьте запрос по скрытому приглашению и верните полученный код `SMMACK1` в установщик Node. Enrollment-код действует 10 минут и погашается при первой успешной регистрации.
|
||||
|
||||
Команды обслуживания учитывают установленную роль и создают root-only backup перед изменениями:
|
||||
|
||||
```bash
|
||||
sudo ./ochenstarik-server-monitor-manager.sh update
|
||||
sudo ./ochenstarik-server-monitor-manager.sh rollback
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-node
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-hub
|
||||
sudo ./ochenstarik-server-monitor-manager.sh uninstall-monitor
|
||||
```
|
||||
|
||||
## Требования
|
||||
|
||||
- Ubuntu Server или совместимая Debian-система с `systemd`;
|
||||
|
|
@ -69,7 +103,39 @@ sudo ./ochenstarik-server-install.sh
|
|||
|
||||
## Получение проекта
|
||||
|
||||
Используйте проверенную загрузку полного архива из раздела «Быстрая установка». Все модули и `ochenstarik-server-supply-chain.sh` должны оставаться рядом. Для обновления нужен новый проверенный полный SHA и соответствующая источнику сумма; распаковывайте в новый каталог. Не обновляйте установщики на рабочих серверах через изменяемую ветку.
|
||||
Рекомендуемый способ — загрузка архива через `codeload.github.com`. Он подходит в том числе для серверов, на которых соединение с основным доменом `github.com` завершается ошибкой `SSL connection timeout`:
|
||||
|
||||
```bash
|
||||
set -e
|
||||
cd "$HOME"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl ca-certificates tar
|
||||
curl -4 -fL \
|
||||
--retry 5 \
|
||||
--retry-delay 10 \
|
||||
--connect-timeout 30 \
|
||||
https://codeload.github.com/ochenstarik-ui/lightweight-server/tar.gz/refs/heads/main \
|
||||
-o lightweight-server.tar.gz
|
||||
mkdir lightweight-server
|
||||
tar -xzf lightweight-server.tar.gz -C lightweight-server --strip-components=1
|
||||
rm lightweight-server.tar.gz
|
||||
cd lightweight-server
|
||||
chmod 700 ./*.sh
|
||||
```
|
||||
|
||||
Если `github.com` доступен с сервера, проект также можно получить через Git:
|
||||
|
||||
```bash
|
||||
set -e
|
||||
cd "$HOME"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git ca-certificates
|
||||
git -c http.version=HTTP/1.1 clone --depth 1 https://github.com/ochenstarik-ui/lightweight-server.git
|
||||
cd lightweight-server
|
||||
chmod 700 ./*.sh
|
||||
```
|
||||
|
||||
Команда `set -e` останавливает последовательность при ошибке загрузки, поэтому команды `cd` и `chmod` не будут выполняться для отсутствующей папки.
|
||||
|
||||
Перед запуском любого выбранного файла можно проверить его синтаксис:
|
||||
|
||||
|
|
@ -104,6 +170,7 @@ bash -n ./ИМЯ-СКРИПТА.sh
|
|||
Затем скрипт создаёт `/swapfile` размером 2 ГБ, добавляет его в `/etc/fstab` и устанавливает `vm.swappiness=20`. Повторный запуск предусмотрен.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-1.sh
|
||||
chmod 700 ochenstarik-server-1.sh
|
||||
bash -n ochenstarik-server-1.sh
|
||||
sudo ./ochenstarik-server-1.sh
|
||||
|
|
@ -132,9 +199,15 @@ sysctl vm.swappiness
|
|||
|
||||
В режиме «только IPv6» системный IPv4 остаётся доступен для исходящих соединений и работы менеджера пакетов, но входящие управляемые порты по IPv4 закрываются UFW. Полное переносимое отключение IPv4 в Linux без привязки к конкретной сетевой конфигурации не выполняется.
|
||||
|
||||
По умолчанию открываются только необходимые SSH-порты на время переноса и `443/tcp`. Порты `80`, `2096`, `40000` и `63636` автоматически не публикуются. Дополнительные порты требуют явного выбора с учётом назначения сервиса. При повторном запуске старые управляемые порты приложений выводятся отдельно: оператор явно выбирает, сохранить их или убрать.
|
||||
Открываются TCP-порты:
|
||||
|
||||
- `22` — временный SSH-порт;
|
||||
- выбранный SSH-порт — будущий порт SSH;
|
||||
- `80`, `443` — HTTP и HTTPS;
|
||||
- `2096`, `40000`, `63636` — порты приложений.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-2.sh
|
||||
chmod 700 ochenstarik-server-2.sh
|
||||
bash -n ochenstarik-server-2.sh
|
||||
sudo ./ochenstarik-server-2.sh
|
||||
|
|
@ -176,6 +249,7 @@ timedatectl
|
|||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y sudo ufw fail2ban openssl openssh-server
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-user-3.sh
|
||||
chmod 700 ochenstarik-server-user-3.sh
|
||||
bash -n ochenstarik-server-user-3.sh
|
||||
sudo ./ochenstarik-server-user-3.sh
|
||||
|
|
@ -187,7 +261,11 @@ sudo ./ochenstarik-server-user-3.sh
|
|||
ssh -p ВЫБРАННЫЙ_ПОРТ ИМЯ_ПОЛЬЗОВАТЕЛЯ@IP_СЕРВЕРА
|
||||
```
|
||||
|
||||
После успешного входа на новый порт подтвердите это в запросе этапа 3: скрипт сам удалит временное правило `22/tcp` для IPv4/IPv6 и запись в управляемом списке. Без подтверждения порт сохраняется. Если SSH остаётся на 22 или управление UFW отключено, очистка не выполняется.
|
||||
Только после успешной проверки можно удалить временное правило порта 22:
|
||||
|
||||
```bash
|
||||
sudo ufw delete allow 22/tcp
|
||||
```
|
||||
|
||||
Чтобы позднее открыть дополнительные порты, повторно запустите скрипт, выберите пункт `2` и введите правила через пробел или запятую:
|
||||
|
||||
|
|
@ -201,13 +279,14 @@ ssh -p ВЫБРАННЫЙ_ПОРТ ИМЯ_ПОЛЬЗОВАТЕЛЯ@IP_СЕРВЕ
|
|||
|
||||
Файл: `ochenstarik-server-tg-4.sh`.
|
||||
|
||||
Создайте бота через `@BotFather`, отправьте ему сообщение и узнайте ID чата. Скрипт запросит токен и ID интерактивно, отправит тестовое сообщение и подключит уведомления через PAM. Hook обрабатывает только `PAM_TYPE=open_session`; закрытие сессии не отправляет второе «успешный вход». Отправка запускается через отсоединённый процесс с закрытым stdin/stdout/stderr и не задерживает SSH-вход при недоступности Telegram.
|
||||
Создайте бота через `@BotFather`, отправьте ему сообщение и узнайте ID чата. Скрипт запросит токен и ID интерактивно, отправит тестовое сообщение и подключит уведомления через PAM.
|
||||
|
||||
Для отдельного запуска:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl openssh-server logrotate
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-tg-4.sh
|
||||
chmod 700 ochenstarik-server-tg-4.sh
|
||||
bash -n ochenstarik-server-tg-4.sh
|
||||
sudo ./ochenstarik-server-tg-4.sh
|
||||
|
|
@ -230,6 +309,7 @@ sudo tail -f /var/log/ochenstarik-ssh-login-telegram.log
|
|||
При повторном запуске появляется меню: подключить VPN с сохранёнными настройками, отключить его без удаления конфигурации, заменить подписку либо показать состояние.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-vpn-5.sh
|
||||
chmod 700 ochenstarik-server-vpn-5.sh
|
||||
bash -n ochenstarik-server-vpn-5.sh
|
||||
sudo ./ochenstarik-server-vpn-5.sh
|
||||
|
|
@ -259,22 +339,26 @@ sudo ./ochenstarik-server-vpn-5.sh --status
|
|||
|
||||
Файл: `ochenstarik-server-panel-warp-6.sh`.
|
||||
|
||||
Панель по умолчанию приватная: `webListen=127.0.0.1`; адрес реального слушателя проверяется через `ss -ltn`. Доступ — через SSH-туннель. Порт панели по умолчанию `2053`.
|
||||
Скрипт предлагает выбрать три разных порта:
|
||||
|
||||
Подписки на отдельном порту (по умолчанию `2096`) выбираются независимо: приватные на loopback, полностью выключенные либо явно публичные с новым случайным секретным путём. При повторном запуске прежний достаточно случайный путь сохраняется по умолчанию; ротация требует подтверждения, поскольку старые URL перестанут работать. Храните URL подписок как секрет. Публичная панель — отдельный явный выбор. В UFW публикуются только выбранные публичные сервисы и `443/tcp`; порт 80 автоматически не открывается.
|
||||
- порт веб-панели 3x-ui, по умолчанию `2053`;
|
||||
- порт подписок 3x-ui, по умолчанию `2096`;
|
||||
- локальный SOCKS5/HTTP proxy-порт WARP, по умолчанию `40000`.
|
||||
|
||||
3x-ui `v3.10.2`, установщик Xray на коммите `05aa47266c94e6fb5ca48cad96afdb4becb5891f` и Xray-core `v26.10.21` закреплены вместе с SHA-256 установщиков и исполняемых payload для amd64/arm64. Этапы 5 и 6 требуют комплектный `ochenstarik-server-supply-chain.sh`. Источник выбирается отдельно: GitHub либо подготовленное оператором HTTPS-зеркало Forgejo. Зеркало должно содержать перечисленные ниже файлы; `ochenstarik-server-supply-chain-pins.txt` — справочный список сумм, проверяемых helper; существование готового зеркального релиза не предполагается. Неизвестные загрузки установщика блокируются. Системные пакеты по-прежнему требуют доступных APT-репозиториев.
|
||||
3x-ui устанавливается официальным интерактивным установщиком. После установки выбранные порты записываются в стандартную SQLite-базу с предварительной резервной копией. Скрипт включает сервис подписок, запускает `x-ui` и проверяет оба TCP-слушателя.
|
||||
|
||||
Файлы зеркала лежат в одном каталоге: `xui-install.sh`, `x-ui-linux-amd64.tar.gz` либо `x-ui-linux-arm64.tar.gz`, `x-ui.sh`, `x-ui.service.debian`, `xray-install.sh`, `Xray-linux-64.zip` либо `Xray-linux-arm64-v8a.zip`. Суммы закреплены в коде; файлы контрольных сумм helper создаёт локально. Для этапа 5 нужны только два файла Xray.
|
||||
|
||||
WARP — отдельный необязательный выбор, по умолчанию выключен. Если выбран, устанавливается из официального APT-репозитория, работает в Local proxy mode через MASQUE и проверяется по `warp=on`. Его proxy слушает только `127.0.0.1:40000` (либо выбранный локальный порт) и не публикуется через UFW.
|
||||
Cloudflare WARP устанавливается из официального APT-репозитория, переводится в Local proxy mode через MASQUE и проверяется запросом к Cloudflare: ответ должен содержать `warp=on`.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-panel-warp-6.sh
|
||||
chmod 700 ochenstarik-server-panel-warp-6.sh
|
||||
bash -n ochenstarik-server-panel-warp-6.sh
|
||||
sudo ./ochenstarik-server-panel-warp-6.sh
|
||||
```
|
||||
|
||||
Публичные порты открываются только для семейств адресов, выбранных на этапе 2.
|
||||
В UFW открываются и проверяются `80/tcp`, `443/tcp`, выбранный порт панели и выбранный порт подписок. WARP слушает только локально на `127.0.0.1:40000` либо другом выбранном порту. Этот порт намеренно не публикуется через UFW, поскольку локальный proxy не требует аутентификации.
|
||||
|
||||
Публичные порты 3x-ui открываются только для семейств адресов, выбранных на этапе 2. В режиме `ipv4` скрипт не создаёт правил IPv6.
|
||||
|
||||
Поддерживаются официально доступные пакеты WARP для Ubuntu `jammy`, `noble`, `resolute` и Debian `bookworm`, `trixie` на `amd64` и `arm64`.
|
||||
|
||||
|
|
@ -294,6 +378,7 @@ sudo ./ochenstarik-server-panel-warp-6.sh
|
|||
- только первичный снимок без автоматического расписания.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-backup-7.sh
|
||||
chmod 700 ochenstarik-server-backup-7.sh
|
||||
bash -n ochenstarik-server-backup-7.sh
|
||||
sudo ./ochenstarik-server-backup-7.sh
|
||||
|
|
@ -323,9 +408,7 @@ mkdir -p /restore
|
|||
zstd -dc /ПУТЬ/К/АРХИВУ.tar.zst | tar --acls --xattrs --numeric-owner -xpf - -C /restore
|
||||
```
|
||||
|
||||
Архив содержит секреты сервера. По умолчанию сохраняется локальный незашифрованный `.tar.zst`. Шифрование age включается отдельно по публичному ключу получателя: поток сразу записывается в `.tar.zst.age`, временного незашифрованного архива нет. Приватный ключ восстановления храните отдельно; заранее проверьте расшифровку через `age --decrypt -i RECOVERY_KEY` перед `zstd -dc | tar`.
|
||||
|
||||
Можно отдельно включить SCP-выгрузку готового архива и `.sha256` на другой хост. Нужны выделенная учётная запись, существующий SSH-ключ и заранее проверенный known_hosts. Используются BatchMode и строгая проверка ключа хоста; ошибки передачи считаются ошибкой бэкапа. Существующий незашифрованный первичный снимок сохраняется: при выборе age скрипт явно остановится, а не объявит его зашифрованным. Перенесите прежний снимок в безопасное хранилище и определите политику первичного снимка перед продолжением. Повторный запуск `initial` проверяет контрольную сумму и повторяет выгрузку, в том числе после прежней ошибки SCP. Локальная ротация не настраивает удалённую ротацию. Локальная копия не защищает от потери сервера. Снимок создаётся с работающей системы; для активно изменяющихся баз данных дополнительно используйте штатный экспорт или согласованный снимок самой базы.
|
||||
Это локальная резервная копия, а не защита от отказа диска или потери сервера. Регулярно переносите готовые архивы и `.sha256` на отдельный сервер или накопитель. Снимок создаётся с работающей системы; для активно изменяющихся баз данных дополнительно используйте штатный экспорт или согласованный снимок самой базы.
|
||||
|
||||
## Полная установка
|
||||
|
||||
|
|
@ -357,8 +440,6 @@ sudo ./ochenstarik-server-ai-agents-8.sh
|
|||
|
||||
Файл: `ochenstarik-server-ai-agents-8.sh`.
|
||||
|
||||
На серверах VPN и узлах Telegram-клиента не устанавливайте этап 8. ИИ-агенты должны работать на отдельной машине.
|
||||
|
||||
Скрипт предлагает множественный выбор из Hermes Agent, OpenClaw, OpenHands, OpenCode, Aider, AutoGPT и Pi Coding Agent. Установщики запускаются от выбранного обычного пользователя, а не напрямую от `root`. Каждый официальный установщик сначала загружается по HTTPS во временный файл и проходит проверку `bash -n`.
|
||||
|
||||
AutoGPT — более тяжёлая Docker-платформа: её официальный установщик может запросить `sudo`, установить зависимости Docker и запустить локальные службы. Pi — лёгкий терминальный агент; после установки его можно запустить командой `pi`, а затем выполнить `/login` для подключения провайдера.
|
||||
|
|
@ -366,6 +447,7 @@ AutoGPT — более тяжёлая Docker-платформа: её офици
|
|||
API-ключи скрипт не запрашивает и не сохраняет. После установки нужно войти под выбранным пользователем и выполнить показанную команду первоначальной настройки. AI-агенты могут запускать команды, изменять файлы, использовать сеть и устанавливать плагины, поэтому перед подключением рабочих данных необходимо проверить разрешения, песочницу и доступ к секретам.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-ai-agents-8.sh
|
||||
chmod 700 ochenstarik-server-ai-agents-8.sh
|
||||
bash -n ochenstarik-server-ai-agents-8.sh
|
||||
sudo ./ochenstarik-server-ai-agents-8.sh
|
||||
|
|
@ -382,6 +464,7 @@ VPN, 3x-ui, WARP, Telegram-уведомления, автоматические
|
|||
Удаление администратора и самих архивов резервных копий требует отдельных подтверждений. Обязательные системные пакеты второго этапа и обновления Ubuntu сохраняются, чтобы не потерять доступ к серверу. Языковые пакеты и дополнительные программы удаляются только если первый скрипт зафиксировал, что до его запуска их не было.
|
||||
|
||||
```bash
|
||||
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-uninstall.sh
|
||||
chmod 700 ochenstarik-server-uninstall.sh
|
||||
bash -n ochenstarik-server-uninstall.sh
|
||||
sudo ./ochenstarik-server-uninstall.sh
|
||||
|
|
|
|||
|
|
@ -17,20 +17,6 @@ require_command() {
|
|||
command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1"
|
||||
}
|
||||
|
||||
ensure_ufw_boot_service() {
|
||||
local state
|
||||
state="$(systemctl is-enabled ufw.service 2>/dev/null || true)"
|
||||
case "$state" in
|
||||
masked|masked-runtime)
|
||||
die "ufw.service замаскирована: сначала проверьте причину маски и включите службу вручную. Правила не изменены."
|
||||
;;
|
||||
esac
|
||||
systemctl enable ufw.service \
|
||||
|| die "Не удалось включить автозапуск ufw.service; правила не изменены"
|
||||
systemctl is-enabled --quiet ufw.service \
|
||||
|| die "Автозапуск ufw.service не подтверждён; правила не изменены"
|
||||
}
|
||||
|
||||
is_valid_port() {
|
||||
local port="$1"
|
||||
[[ "$port" =~ ^[0-9]{1,5}$ ]] || return 1
|
||||
|
|
@ -123,44 +109,11 @@ add_managed_rule() {
|
|||
RULE_SEEN["$rule"]=1
|
||||
}
|
||||
|
||||
discover_current_ssh_ports() {
|
||||
local managed_file="${1:-$MANAGED_PORTS_CONFIG}"
|
||||
local client_ip client_port server_ip server_port port
|
||||
local -A seen_ports=()
|
||||
|
||||
if [[ -n "${SSH_CONNECTION:-}" ]]; then
|
||||
read -r client_ip client_port server_ip server_port <<< "$SSH_CONNECTION"
|
||||
if is_valid_port "${server_port:-}"; then
|
||||
printf '%s\n' "$((10#$server_port))"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
while IFS= read -r port; do
|
||||
is_valid_port "$port" || continue
|
||||
port="$((10#$port))"
|
||||
[[ -z "${seen_ports[$port]:-}" ]] || continue
|
||||
printf '%s\n' "$port"
|
||||
seen_ports["$port"]=1
|
||||
done < <(ss -H -ltnp 2>/dev/null | awk '$NF ~ /sshd/ { address=$4; sub(/^.*:/, "", address); if (address ~ /^[0-9]+$/) print address }')
|
||||
fi
|
||||
|
||||
if ((${#seen_ports[@]} == 0)) && [[ ! -e "$managed_file" ]]; then
|
||||
# First run from sudo or a local console: preserve the conventional SSH
|
||||
# port when no session metadata or listening socket is discoverable.
|
||||
printf '22\n'
|
||||
fi
|
||||
}
|
||||
|
||||
load_managed_rules() {
|
||||
local rule port protocol
|
||||
MANAGED_RULES=()
|
||||
RULE_SEEN=()
|
||||
PREVIOUS_MANAGED_RULES=()
|
||||
if [[ ! -e "$MANAGED_PORTS_CONFIG" ]]; then
|
||||
return 0
|
||||
fi
|
||||
[[ -e "$MANAGED_PORTS_CONFIG" ]] || return 0
|
||||
[[ -f "$MANAGED_PORTS_CONFIG" && ! -L "$MANAGED_PORTS_CONFIG" ]] \
|
||||
|| die "$MANAGED_PORTS_CONFIG должен быть обычным файлом"
|
||||
|
||||
|
|
@ -174,58 +127,20 @@ load_managed_rules() {
|
|||
is_valid_port "$port" || die "Некорректный порт в $MANAGED_PORTS_CONFIG: $port"
|
||||
add_managed_rule "$((10#$port))/${protocol}"
|
||||
done < "$MANAGED_PORTS_CONFIG"
|
||||
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
|
||||
}
|
||||
|
||||
add_default_rules() {
|
||||
local saved_ssh_port current_ssh_port
|
||||
local -a current_ssh_ports=()
|
||||
local saved_ssh_port
|
||||
|
||||
saved_ssh_port="${SSH_PORT:-$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)}"
|
||||
is_valid_port "$saved_ssh_port" || saved_ssh_port="$DEFAULT_SSH_PORT"
|
||||
add_managed_rule "$((10#$saved_ssh_port))/tcp"
|
||||
add_managed_rule 443/tcp
|
||||
|
||||
# Keep the port carrying this setup session open during an SSH migration.
|
||||
mapfile -t current_ssh_ports < <(discover_current_ssh_ports)
|
||||
for current_ssh_port in "${current_ssh_ports[@]}"; do
|
||||
add_managed_rule "$current_ssh_port/tcp"
|
||||
for rule in \
|
||||
22/tcp "$((10#$saved_ssh_port))/tcp" 80/tcp 2096/tcp 443/tcp \
|
||||
40000/tcp 63636/tcp; do
|
||||
add_managed_rule "$rule"
|
||||
done
|
||||
}
|
||||
|
||||
confirm_legacy_rules() {
|
||||
local rule answer current_ssh_port
|
||||
local -a legacy_rules=() retained_rules=()
|
||||
local -a current_ssh_ports=()
|
||||
local -A defaults=() seen=()
|
||||
defaults["$((10#$SSH_PORT))/tcp"]=1
|
||||
defaults[443/tcp]=1
|
||||
mapfile -t current_ssh_ports < <(discover_current_ssh_ports)
|
||||
for current_ssh_port in "${current_ssh_ports[@]}"; do defaults["$current_ssh_port/tcp"]=1; done
|
||||
|
||||
for rule in "${MANAGED_RULES[@]}"; do
|
||||
if [[ -n "${defaults[$rule]:-}" ]]; then
|
||||
retained_rules+=("$rule")
|
||||
seen["$rule"]=1
|
||||
else
|
||||
legacy_rules+=("$rule")
|
||||
fi
|
||||
done
|
||||
((${#legacy_rules[@]})) || return 0
|
||||
|
||||
printf '\nНайдены дополнительные правила UFW из прежней конфигурации; их назначение неизвестно:\n'
|
||||
printf ' %s\n' "${legacy_rules[@]}"
|
||||
read -rp 'Сохранить весь этот список? [y/N]: ' answer \
|
||||
|| die "Ввод прерван; правила UFW не изменены"
|
||||
if [[ "$answer" == y || "$answer" == Y ]]; then
|
||||
retained_rules+=("${legacy_rules[@]}")
|
||||
for rule in "${legacy_rules[@]}"; do seen["$rule"]=1; done
|
||||
fi
|
||||
MANAGED_RULES=("${retained_rules[@]}")
|
||||
RULE_SEEN=()
|
||||
for rule in "${!seen[@]}"; do RULE_SEEN["$rule"]=1; done
|
||||
}
|
||||
|
||||
collect_additional_ports() {
|
||||
local input normalized token port protocol rule
|
||||
local -a tokens additions=()
|
||||
|
|
@ -328,9 +243,9 @@ verify_ipv6_connectivity() {
|
|||
}
|
||||
|
||||
delete_managed_ufw_rules() {
|
||||
local rule port protocol attempt status
|
||||
local rule port protocol attempt
|
||||
|
||||
for rule in "${PREVIOUS_MANAGED_RULES[@]}"; do
|
||||
for rule in "${MANAGED_RULES[@]}"; do
|
||||
port="${rule%/*}"
|
||||
protocol="${rule#*/}"
|
||||
for attempt in {1..5}; do
|
||||
|
|
@ -341,15 +256,6 @@ delete_managed_ufw_rules() {
|
|||
ufw --force delete allow from ::/0 to any port "$port" proto "$protocol" \
|
||||
>/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
status="$(LANG=C ufw status)" || die "Не удалось проверить состояние UFW после удаления старых правил"
|
||||
for rule in "${PREVIOUS_MANAGED_RULES[@]}"; do
|
||||
if awk -v rule="$rule" \
|
||||
'$1 == rule && ($2 == "ALLOW" || ($2 == "(v6)" && $3 == "ALLOW")) { found=1 } END { exit !found }' \
|
||||
<<< "$status"; then
|
||||
die "Старое правило UFW ${rule} всё ещё разрешено; новая конфигурация не сохранена"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
allow_rule_for_selected_families() {
|
||||
|
|
@ -397,7 +303,6 @@ apply_ip_mode_and_firewall() {
|
|||
|
||||
[[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root"
|
||||
declare -a MANAGED_RULES=()
|
||||
declare -a PREVIOUS_MANAGED_RULES=()
|
||||
declare -A RULE_SEEN=()
|
||||
ACTION=""
|
||||
SSH_PORT=""
|
||||
|
|
@ -417,29 +322,23 @@ if [[ "$ACTION" == install ]]; then
|
|||
apt-get install -y \
|
||||
sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2
|
||||
|
||||
for command_name in ip sed sysctl ufw systemctl; do
|
||||
for command_name in ip sed sysctl ufw; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
ensure_ufw_boot_service
|
||||
install -d -m 700 -o root -g root "$CONFIG_DIR"
|
||||
load_managed_rules
|
||||
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
|
||||
confirm_legacy_rules
|
||||
add_default_rules
|
||||
apply_ip_mode_and_firewall
|
||||
save_configuration
|
||||
apply_ip_mode_and_firewall
|
||||
log "SSH-порт ${SSH_PORT} сохранён для этапа 3"
|
||||
else
|
||||
for command_name in ip sed sysctl ufw systemctl; do
|
||||
for command_name in ip sed sysctl ufw; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
ensure_ufw_boot_service
|
||||
install -d -m 700 -o root -g root "$CONFIG_DIR"
|
||||
SSH_PORT="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)"
|
||||
is_valid_port "$SSH_PORT" || SSH_PORT="$DEFAULT_SSH_PORT"
|
||||
load_managed_rules
|
||||
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
|
||||
confirm_legacy_rules
|
||||
add_default_rules
|
||||
|
||||
if [[ "$ACTION" == ip-mode ]]; then
|
||||
|
|
@ -449,8 +348,8 @@ else
|
|||
case "$IP_MODE" in ipv4|ipv6|both) ;; *) choose_ip_mode ;; esac
|
||||
collect_additional_ports
|
||||
fi
|
||||
apply_ip_mode_and_firewall
|
||||
save_configuration
|
||||
apply_ip_mode_and_firewall
|
||||
fi
|
||||
|
||||
log "Текущая конфигурация"
|
||||
|
|
|
|||
|
|
@ -77,54 +77,6 @@ choose_backup_root() {
|
|||
esac
|
||||
}
|
||||
|
||||
choose_protection() {
|
||||
local answer known_hosts
|
||||
|
||||
read -rp 'Шифровать архивы через age? [y/N]: ' answer || die "Ввод был прерван"
|
||||
case "${answer,,}" in
|
||||
y|yes)
|
||||
BACKUP_ENCRYPTION=age
|
||||
read -rp 'Публичный age recipient (age1...): ' AGE_RECIPIENT \
|
||||
|| die "Ввод был прерван"
|
||||
[[ "$AGE_RECIPIENT" =~ ^age1[0-9a-z]{20,}$ ]] \
|
||||
|| die "Укажите корректный публичный age recipient"
|
||||
;;
|
||||
''|n|no) BACKUP_ENCRYPTION=none ;;
|
||||
*) die "Введите y или n" ;;
|
||||
esac
|
||||
|
||||
read -rp 'Выгружать архивы на другой SSH-хост? [y/N]: ' answer \
|
||||
|| die "Ввод был прерван"
|
||||
case "${answer,,}" in
|
||||
y|yes)
|
||||
read -rp 'SSH-цель в формате user@host: ' OFFSITE_TARGET \
|
||||
|| die "Ввод был прерван"
|
||||
[[ "$OFFSITE_TARGET" =~ ^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$ ]] \
|
||||
|| die "Некорректная SSH-цель"
|
||||
read -rp 'Каталог на целевом хосте (абсолютный путь): ' OFFSITE_PATH \
|
||||
|| die "Ввод был прерван"
|
||||
[[ "$OFFSITE_PATH" =~ ^/[A-Za-z0-9._/-]+$ && "$OFFSITE_PATH" != / \
|
||||
&& "$OFFSITE_PATH" != *..* ]] \
|
||||
|| die "Некорректный удалённый каталог"
|
||||
read -rp 'Локальный файл known_hosts с заранее проверенным ключом: ' known_hosts \
|
||||
|| die "Ввод был прерван"
|
||||
[[ -f "$known_hosts" && ! -L "$known_hosts" && -s "$known_hosts" ]] \
|
||||
|| die "Нужен существующий непустой обычный known_hosts файл"
|
||||
[[ "$(stat -c %u "$known_hosts")" == 0 ]] \
|
||||
|| die "known_hosts должен принадлежать root"
|
||||
[[ "$(stat -c %a "$known_hosts")" =~ ^[0-7]*[0-5][0-5]$ ]] \
|
||||
|| die "known_hosts не должен быть доступен для записи группе или всем"
|
||||
OFFSITE_KNOWN_HOSTS_SOURCE="$(readlink -f -- "$known_hosts")"
|
||||
OFFSITE_KNOWN_HOSTS="${CONFIG_DIR}/backup-known_hosts"
|
||||
;;
|
||||
''|n|no)
|
||||
OFFSITE_TARGET=""; OFFSITE_PATH=""; OFFSITE_KNOWN_HOSTS=""
|
||||
OFFSITE_KNOWN_HOSTS_SOURCE=""
|
||||
;;
|
||||
*) die "Введите y или n" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
write_backup_runner() {
|
||||
cat > "$BACKUP_RUNNER" <<'BACKUP_RUNNER_EOF'
|
||||
#!/usr/bin/env bash
|
||||
|
|
@ -147,30 +99,13 @@ protect_initial_files() {
|
|||
local file
|
||||
command -v chattr >/dev/null 2>&1 || return 0
|
||||
for file in "$BACKUP_ROOT"/initial/initial-*.tar.zst \
|
||||
"$BACKUP_ROOT"/initial/initial-*.tar.zst.sha256 \
|
||||
"$BACKUP_ROOT"/initial/initial-*.tar.zst.age \
|
||||
"$BACKUP_ROOT"/initial/initial-*.tar.zst.age.sha256; do
|
||||
[[ -f "$file" && ! -L "$file" ]] || continue
|
||||
"$BACKUP_ROOT"/initial/initial-*.tar.zst.sha256; do
|
||||
[[ -e "$file" ]] || continue
|
||||
chattr +i -- "$file" 2>/dev/null \
|
||||
|| warn "Файловая система не поддерживает immutable для $file"
|
||||
done
|
||||
}
|
||||
|
||||
upload_offsite() {
|
||||
local archive="$1" checksum="$2"
|
||||
[[ -z "$OFFSITE_TARGET" ]] && return 0
|
||||
scp -B \
|
||||
-o BatchMode=yes \
|
||||
-o ConnectTimeout=15 \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o "UserKnownHostsFile=$OFFSITE_KNOWN_HOSTS" \
|
||||
-o ForwardAgent=no \
|
||||
-o ClearAllForwardings=yes \
|
||||
"$archive" "$checksum" "${OFFSITE_TARGET}:${OFFSITE_PATH}/" \
|
||||
|| die "Выгрузка на заданный SSH-хост не удалась; резервная копия не считается завершённой"
|
||||
log "Архив и checksum выгружены на настроенный SSH-хост"
|
||||
}
|
||||
|
||||
check_free_space() {
|
||||
local root_device backup_device root_used backup_used source_used
|
||||
local available reserve estimated_required backup_total
|
||||
|
|
@ -201,8 +136,7 @@ rotate_backups() {
|
|||
local -a archives
|
||||
|
||||
mapfile -t archives < <(
|
||||
find "$target_dir" -maxdepth 1 -type f \
|
||||
\( -name "${backup_kind}-*.tar.zst" -o -name "${backup_kind}-*.tar.zst.age" \) \
|
||||
find "$target_dir" -maxdepth 1 -type f -name "${backup_kind}-*.tar.zst" \
|
||||
-printf '%f\n' | sort -r
|
||||
)
|
||||
for ((index = keep_count; index < ${#archives[@]}; index++)); do
|
||||
|
|
@ -215,12 +149,6 @@ rotate_backups() {
|
|||
read_backup_config() {
|
||||
local line key value
|
||||
BACKUP_ROOT=""
|
||||
BACKUP_ENCRYPTION=none
|
||||
AGE_RECIPIENT=""
|
||||
OFFSITE_TARGET=""
|
||||
OFFSITE_PATH=""
|
||||
OFFSITE_KNOWN_HOSTS=""
|
||||
OFFSITE_KNOWN_HOSTS_SOURCE=""
|
||||
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \
|
||||
|| die "Не найден безопасный файл настроек: $CONFIG_FILE"
|
||||
[[ "$(stat -c %u "$CONFIG_FILE")" == 0 ]] \
|
||||
|
|
@ -240,92 +168,30 @@ read_backup_config() {
|
|||
|| die "Некорректный BACKUP_ROOT в $CONFIG_FILE"
|
||||
BACKUP_ROOT="$value"
|
||||
;;
|
||||
BACKUP_ENCRYPTION)
|
||||
[[ "$value" == none || "$value" == age ]] \
|
||||
|| die "Некорректный BACKUP_ENCRYPTION в $CONFIG_FILE"
|
||||
BACKUP_ENCRYPTION="$value"
|
||||
;;
|
||||
AGE_RECIPIENT)
|
||||
[[ "$value" =~ ^age1[0-9a-z]{20,}$ ]] \
|
||||
|| die "Некорректный AGE_RECIPIENT в $CONFIG_FILE"
|
||||
AGE_RECIPIENT="$value"
|
||||
;;
|
||||
OFFSITE_TARGET)
|
||||
[[ "$value" =~ ^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$ ]] \
|
||||
|| die "Некорректный OFFSITE_TARGET в $CONFIG_FILE"
|
||||
OFFSITE_TARGET="$value"
|
||||
;;
|
||||
OFFSITE_PATH)
|
||||
[[ "$value" =~ ^/[A-Za-z0-9._/-]+$ && "$value" != / \
|
||||
&& "$value" != *..* ]] \
|
||||
|| die "Некорректный OFFSITE_PATH в $CONFIG_FILE"
|
||||
OFFSITE_PATH="$value"
|
||||
;;
|
||||
OFFSITE_KNOWN_HOSTS)
|
||||
[[ "$value" =~ ^/[A-Za-z0-9._/-]+$ ]] \
|
||||
|| die "Некорректный OFFSITE_KNOWN_HOSTS в $CONFIG_FILE"
|
||||
OFFSITE_KNOWN_HOSTS="$value"
|
||||
;;
|
||||
*)
|
||||
die "Неизвестный ключ в $CONFIG_FILE: $key"
|
||||
;;
|
||||
esac
|
||||
done < "$CONFIG_FILE"
|
||||
if [[ "$BACKUP_ENCRYPTION" == age ]]; then
|
||||
[[ -n "$AGE_RECIPIENT" ]] || die "Для шифрования нужен AGE_RECIPIENT"
|
||||
command -v age >/dev/null 2>&1 || die "Для шифрования не найдена команда age"
|
||||
elif [[ -n "$AGE_RECIPIENT" ]]; then
|
||||
die "AGE_RECIPIENT задан, но шифрование отключено"
|
||||
fi
|
||||
if [[ -n "$OFFSITE_TARGET" || -n "$OFFSITE_PATH" || -n "$OFFSITE_KNOWN_HOSTS" ]]; then
|
||||
[[ -n "$OFFSITE_TARGET" && -n "$OFFSITE_PATH" && -n "$OFFSITE_KNOWN_HOSTS" ]] \
|
||||
|| die "Настройки выгрузки должны быть заполнены полностью"
|
||||
[[ -f "$OFFSITE_KNOWN_HOSTS" && ! -L "$OFFSITE_KNOWN_HOSTS" \
|
||||
&& -s "$OFFSITE_KNOWN_HOSTS" && "$(stat -c %u "$OFFSITE_KNOWN_HOSTS")" == 0 \
|
||||
&& "$(stat -c %a "$OFFSITE_KNOWN_HOSTS")" =~ ^[0-7]*[0-5][0-5]$ ]] \
|
||||
|| die "known_hosts отсутствует или имеет небезопасные права"
|
||||
command -v scp >/dev/null 2>&1 || die "Для выгрузки не найдена команда scp"
|
||||
fi
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
local backup_kind="$1" keep_count="$2" timestamp target_dir
|
||||
local archive temporary checksum checksum_temporary exclude_backup_root existing
|
||||
local -a initial_archives
|
||||
local archive temporary checksum checksum_temporary exclude_backup_root
|
||||
|
||||
target_dir="${BACKUP_ROOT}/${backup_kind}"
|
||||
mkdir -p -m 700 -- "$target_dir"
|
||||
|
||||
if [[ "$backup_kind" == initial ]]; then
|
||||
mapfile -t initial_archives < <(
|
||||
find "$target_dir" -maxdepth 1 -type f \
|
||||
\( -name 'initial-*.tar.zst' -o -name 'initial-*.tar.zst.age' \) \
|
||||
-printf '%f\n' | sort -r
|
||||
)
|
||||
if ((${#initial_archives[@]} > 0)); then
|
||||
existing="${target_dir}/${initial_archives[0]}"
|
||||
checksum="${existing}.sha256"
|
||||
[[ -f "$existing" && ! -L "$existing" \
|
||||
&& -f "$checksum" && ! -L "$checksum" ]] \
|
||||
|| die "Первичный снимок или его контрольная сумма отсутствует либо небезопасна"
|
||||
(
|
||||
cd -- "$target_dir"
|
||||
sha256sum --check --status "$(basename "$checksum")"
|
||||
) || die "Контрольная сумма первичного снимка не совпадает"
|
||||
if [[ "$BACKUP_ENCRYPTION" == age && "$existing" != *.tar.zst.age ]]; then
|
||||
die "Первичный снимок уже создан без шифрования; он сохранён, но не соответствует выбранной политике age. Укажите прежнюю политику или архивируйте систему отдельно."
|
||||
fi
|
||||
if [[ "$backup_kind" == initial ]] &&
|
||||
find "$target_dir" -maxdepth 1 -type f -name 'initial-*.tar.zst' -print -quit | grep -q .; then
|
||||
log "Первичный снимок уже существует и не будет перезаписан"
|
||||
protect_initial_files
|
||||
upload_offsite "$existing" "$checksum"
|
||||
log "Первичный снимок уже существует и не будет перезаписан; checksum проверен"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
check_free_space
|
||||
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
archive="${target_dir}/${backup_kind}-${timestamp}.tar.zst"
|
||||
[[ "$BACKUP_ENCRYPTION" != age ]] || archive="${archive}.age"
|
||||
temporary="${archive}.partial"
|
||||
checksum="${archive}.sha256"
|
||||
checksum_temporary="${checksum}.partial"
|
||||
|
|
@ -367,13 +233,7 @@ create_backup() {
|
|||
--exclude='./swapfile' \
|
||||
--exclude="$exclude_backup_root" \
|
||||
--exclude="${exclude_backup_root}/*" \
|
||||
. | zstd -T0 -3 --quiet | {
|
||||
if [[ "$BACKUP_ENCRYPTION" == age ]]; then
|
||||
age --encrypt --recipient "$AGE_RECIPIENT" --output "$temporary"
|
||||
else
|
||||
cat > "$temporary"
|
||||
fi
|
||||
}
|
||||
. | zstd -T0 -3 --quiet -o "$temporary"
|
||||
|
||||
[[ -s "$temporary" ]] || die "Получился пустой архив"
|
||||
chmod 600 "$temporary"
|
||||
|
|
@ -391,7 +251,6 @@ create_backup() {
|
|||
else
|
||||
rotate_backups "$backup_kind" "$keep_count"
|
||||
fi
|
||||
upload_offsite "$archive" "$checksum"
|
||||
log "Готово: $archive ($(human_size "$(stat -c %s "$archive")"))"
|
||||
}
|
||||
|
||||
|
|
@ -517,35 +376,18 @@ done
|
|||
|
||||
choose_schedules
|
||||
choose_backup_root
|
||||
choose_protection
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
log "Устанавливаю зависимости"
|
||||
apt-get update
|
||||
apt-get install -y coreutils e2fsprogs findutils tar util-linux zstd
|
||||
[[ "$BACKUP_ENCRYPTION" != age ]] || apt-get install -y age
|
||||
[[ -z "$OFFSITE_TARGET" ]] || apt-get install -y openssh-client
|
||||
for command_name in awk basename chattr df du find flock grep numfmt sha256sum sort stat tar zstd; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
|
||||
install -d -o root -g root -m 700 "$CONFIG_DIR" "$BACKUP_ROOT"
|
||||
if [[ -n "$OFFSITE_TARGET" ]]; then
|
||||
[[ ! -L "$OFFSITE_KNOWN_HOSTS" ]] \
|
||||
|| die "Отказ от записи через символическую ссылку: $OFFSITE_KNOWN_HOSTS"
|
||||
install -o root -g root -m 600 -- "$OFFSITE_KNOWN_HOSTS_SOURCE" "$OFFSITE_KNOWN_HOSTS"
|
||||
fi
|
||||
[[ ! -L "$CONFIG_FILE" ]] || die "Отказ от записи через символическую ссылку: $CONFIG_FILE"
|
||||
{
|
||||
printf 'BACKUP_ROOT=%s\n' "$BACKUP_ROOT"
|
||||
printf 'BACKUP_ENCRYPTION=%s\n' "$BACKUP_ENCRYPTION"
|
||||
[[ "$BACKUP_ENCRYPTION" != age ]] || printf 'AGE_RECIPIENT=%s\n' "$AGE_RECIPIENT"
|
||||
if [[ -n "$OFFSITE_TARGET" ]]; then
|
||||
printf 'OFFSITE_TARGET=%s\n' "$OFFSITE_TARGET"
|
||||
printf 'OFFSITE_PATH=%s\n' "$OFFSITE_PATH"
|
||||
printf 'OFFSITE_KNOWN_HOSTS=%s\n' "$OFFSITE_KNOWN_HOSTS"
|
||||
fi
|
||||
} > "$CONFIG_FILE"
|
||||
printf 'BACKUP_ROOT=%s\n' "$BACKUP_ROOT" > "$CONFIG_FILE"
|
||||
chown root:root "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
|
||||
|
|
|
|||
1367
ochenstarik-server-monitor-manager.sh
Normal file
1367
ochenstarik-server-monitor-manager.sh
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly XUI_INSTALL_URL="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh"
|
||||
readonly XUI_BINARY="/usr/local/x-ui/x-ui"
|
||||
readonly XUI_DB="/etc/x-ui/x-ui.db"
|
||||
readonly CLOUDFLARE_KEY_URL="https://pkg.cloudflareclient.com/pubkey.gpg"
|
||||
|
|
@ -12,12 +13,8 @@ readonly DEFAULT_WARP_PORT="40000"
|
|||
readonly SSH_PORT_CONFIG="/etc/ochenstarik-server/ssh-port.conf"
|
||||
readonly IP_FAMILY_CONFIG="/etc/ochenstarik-server/ip-family.conf"
|
||||
readonly MANAGED_PORTS_CONFIG="/etc/ochenstarik-server/ufw-managed-ports.conf"
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SUPPLY_CHAIN_HELPER="${SCRIPT_DIR}/ochenstarik-server-supply-chain.sh"
|
||||
|
||||
TMP_DIR=""
|
||||
XUI_GUARD_DIR=""
|
||||
XUI_BOOTSTRAP_RELEASE_MARKER=""
|
||||
|
||||
log() { printf '[+] %s\n' "$*"; }
|
||||
warn() { printf '[!] %s\n' "$*" >&2; }
|
||||
|
|
@ -115,139 +112,19 @@ wait_for_tcp_listener() {
|
|||
die "${description} не открыл TCP-порт ${port}"
|
||||
}
|
||||
|
||||
verify_private_listener() {
|
||||
local port="$1" description="$2" listeners address found=no
|
||||
listeners="$(ss -H -ltn "sport = :${port}")"
|
||||
[[ -n "$listeners" ]] || die "${description} не слушает TCP-порт ${port}"
|
||||
while read -r address; do
|
||||
[[ -n "$address" ]] || continue
|
||||
[[ "$address" == "127.0.0.1:${port}" ]] \
|
||||
|| die "${description} слушает внешний или IPv6-адрес ${address}; ожидался только 127.0.0.1:${port}"
|
||||
found=yes
|
||||
done < <(awk '{print $4}' <<< "$listeners")
|
||||
[[ "$found" == yes ]] || die "Не удалось проверить loopback bind ${port}"
|
||||
}
|
||||
|
||||
valid_subscription_path() {
|
||||
local path="$1"
|
||||
[[ -n "$path" && ${#path} -le 1024 && "$path" =~ ^/?[A-Za-z0-9._~-]+(/[A-Za-z0-9._~-]+)*/?$ ]] \
|
||||
|| return 1
|
||||
[[ "$path" != *'..'* && "$path" != *'//'* ]]
|
||||
}
|
||||
|
||||
secure_public_subscription_path() {
|
||||
local path="$1" component
|
||||
component="${path#/}"
|
||||
component="${component%/}"
|
||||
[[ "$component" =~ ^[A-Za-z0-9_-]{32,}$ ]]
|
||||
}
|
||||
|
||||
generate_subscription_path() {
|
||||
local secret
|
||||
secret="$(openssl rand -hex 24)" || die "Не удалось создать случайный путь подписки"
|
||||
[[ "$secret" =~ ^[0-9a-f]{48}$ ]] || die "Генератор создал путь подписки неожиданного формата"
|
||||
XUI_SUB_PATH="/${secret}/"
|
||||
}
|
||||
|
||||
load_existing_subscription_path() {
|
||||
local db="${1:-$XUI_DB}"
|
||||
XUI_EXISTING_SUB_PATH=""
|
||||
[[ -f "$db" && ! -L "$db" ]] || return 0
|
||||
XUI_EXISTING_SUB_PATH="$(sqlite3 "$db" "SELECT value FROM settings WHERE key='subPath' LIMIT 1;" 2>/dev/null || true)"
|
||||
valid_subscription_path "$XUI_EXISTING_SUB_PATH" || XUI_EXISTING_SUB_PATH=""
|
||||
}
|
||||
|
||||
choose_public_access() {
|
||||
local answer
|
||||
printf '\nДоступ к панели 3x-ui:\n'
|
||||
printf ' 1) Приватный режим: слушать только 127.0.0.1, доступ через SSH tunnel (рекомендуется)\n'
|
||||
printf ' 2) Публичный режим: слушать все интерфейсы и открыть порт панели в UFW\n'
|
||||
printf '\nДоступ к панели 3x-ui и подпискам:\n'
|
||||
printf ' 1) Приватный режим: не открывать порты панели и подписок в UFW (рекомендуется)\n'
|
||||
printf ' 2) Публичный режим: открыть порты панели и подписок всему Интернету\n'
|
||||
while :; do
|
||||
read -rp 'Доступ к панели [1]: ' answer || answer=1
|
||||
read -rp 'Режим доступа [1]: ' answer
|
||||
answer="${answer:-1}"
|
||||
case "$answer" in
|
||||
1) XUI_PUBLIC_ACCESS=no; XUI_WEB_LISTEN=127.0.0.1; return 0 ;;
|
||||
1) XUI_PUBLIC_ACCESS=no; return 0 ;;
|
||||
2)
|
||||
warn "Публичная панель доступна из Интернета. Используйте TLS, новые учётные данные и 2FA"
|
||||
warn "Публичный доступ к панели опасен. Используйте сложный путь, новые учётные данные, TLS и 2FA"
|
||||
XUI_PUBLIC_ACCESS=yes
|
||||
XUI_WEB_LISTEN=""
|
||||
return 0
|
||||
;;
|
||||
*) warn "Введите 1 или 2" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
choose_subscription_access() {
|
||||
local answer rotate_answer existing_path="${XUI_EXISTING_SUB_PATH:-}"
|
||||
printf '\nДоступ к подпискам 3x-ui (выбор не зависит от доступа к панели):\n'
|
||||
printf ' 1) Приватный режим: включены только на 127.0.0.1 (рекомендуется)\n'
|
||||
printf ' 2) Выключить сервис подписок\n'
|
||||
printf ' 3) Публичный доступ: отдельный случайный секретный путь и открытый порт\n'
|
||||
while :; do
|
||||
read -rp 'Доступ к подпискам [1]: ' answer || answer=1
|
||||
answer="${answer:-1}"
|
||||
case "$answer" in
|
||||
1)
|
||||
XUI_SUBSCRIPTION_ACCESS=private
|
||||
XUI_SUB_LISTEN=127.0.0.1
|
||||
XUI_SUB_PATH="${existing_path:-/sub/}"
|
||||
return 0
|
||||
;;
|
||||
2)
|
||||
XUI_SUBSCRIPTION_ACCESS=disabled
|
||||
XUI_SUB_LISTEN=127.0.0.1
|
||||
XUI_SUB_PATH="${existing_path:-/sub/}"
|
||||
return 0
|
||||
;;
|
||||
3)
|
||||
command -v openssl >/dev/null 2>&1 || die "Для случайного пути подписки нужна команда openssl"
|
||||
if [[ -n "$existing_path" ]] && secure_public_subscription_path "$existing_path"; then
|
||||
warn "Найден действующий секретный путь подписки; его сохранение оставит существующие URL рабочими"
|
||||
read -rp 'Создать новый путь и отключить старые URL? [y/N]: ' rotate_answer || rotate_answer=no
|
||||
if [[ "$rotate_answer" == y || "$rotate_answer" == Y ]]; then
|
||||
generate_subscription_path
|
||||
else
|
||||
XUI_SUB_PATH="$existing_path"
|
||||
fi
|
||||
elif [[ -n "$existing_path" ]]; then
|
||||
warn "Текущий путь подписки не выглядит достаточно случайным. Ротация отключит существующие URL."
|
||||
read -rp 'Создать новый криптографически случайный путь для публичного доступа? [y/N]: ' rotate_answer \
|
||||
|| rotate_answer=no
|
||||
if [[ "$rotate_answer" == y || "$rotate_answer" == Y ]]; then
|
||||
generate_subscription_path
|
||||
else
|
||||
warn "Публичный доступ к подпискам не выбран; выберите приватный режим или отключение"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
generate_subscription_path
|
||||
fi
|
||||
XUI_SUBSCRIPTION_ACCESS=public
|
||||
XUI_SUB_LISTEN=""
|
||||
return 0
|
||||
;;
|
||||
*) warn "Введите 1, 2 или 3" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
choose_xui_source() {
|
||||
local answer mirror_base
|
||||
printf '\nИсточник проверенных установочных файлов 3x-ui и Xray:\n'
|
||||
printf ' 1) GitHub (по умолчанию)\n'
|
||||
printf ' 2) Forgejo: указать HTTPS base URL без учётных данных\n'
|
||||
while :; do
|
||||
read -rp 'Источник [1]: ' answer || answer=1
|
||||
answer="${answer:-1}"
|
||||
case "$answer" in
|
||||
1) XUI_SUPPLY_SOURCE=github; XUI_SUPPLY_MIRROR=""; return 0 ;;
|
||||
2)
|
||||
read -rp 'Forgejo HTTPS base URL: ' mirror_base \
|
||||
|| die "Forgejo URL не получен"
|
||||
[[ -n "$mirror_base" ]] || die "Forgejo URL не может быть пустым"
|
||||
XUI_SUPPLY_SOURCE=forgejo
|
||||
XUI_SUPPLY_MIRROR="$mirror_base"
|
||||
return 0
|
||||
;;
|
||||
*) warn "Введите 1 или 2" ;;
|
||||
|
|
@ -294,127 +171,8 @@ COMMIT;
|
|||
SQL
|
||||
}
|
||||
|
||||
set_xui_desired_listeners() {
|
||||
if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then
|
||||
XUI_DESIRED_WEB_LISTEN=""
|
||||
else
|
||||
XUI_DESIRED_WEB_LISTEN=127.0.0.1
|
||||
fi
|
||||
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" == public ]]; then
|
||||
XUI_DESIRED_SUB_LISTEN=""
|
||||
else
|
||||
XUI_DESIRED_SUB_LISTEN=127.0.0.1
|
||||
fi
|
||||
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" == disabled ]]; then
|
||||
XUI_DESIRED_SUB_ENABLED=false
|
||||
else
|
||||
XUI_DESIRED_SUB_ENABLED=true
|
||||
fi
|
||||
}
|
||||
|
||||
create_xui_start_guard() {
|
||||
local directory="$1" marker="$2" binary="$3" db="$4"
|
||||
local real_systemctl="${5:-$(command -v systemctl)}"
|
||||
local real_rc_service="${6:-$(command -v rc-service || true)}"
|
||||
local real_sqlite3="${7:-$(command -v sqlite3)}"
|
||||
local systemctl_q marker_q binary_q db_q rc_service_q sqlite3_q
|
||||
[[ -x "$real_systemctl" ]] || die "systemctl недоступен для безопасной установки 3x-ui"
|
||||
[[ -x "$real_sqlite3" ]] || die "sqlite3 недоступен для безопасной установки 3x-ui"
|
||||
mkdir -m 700 -p -- "$directory"
|
||||
printf -v systemctl_q '%q' "$real_systemctl"
|
||||
printf -v marker_q '%q' "$marker"
|
||||
printf -v binary_q '%q' "$binary"
|
||||
printf -v db_q '%q' "$db"
|
||||
printf -v rc_service_q '%q' "$real_rc_service"
|
||||
printf -v sqlite3_q '%q' "$real_sqlite3"
|
||||
|
||||
cat > "$directory/systemctl" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
REAL_SYSTEMCTL=${systemctl_q}
|
||||
RELEASE_MARKER=${marker_q}
|
||||
XUI_BINARY=${binary_q}
|
||||
XUI_DB=${db_q}
|
||||
SQLITE3_BIN=${sqlite3_q}
|
||||
service=""
|
||||
case "\${1:-}" in
|
||||
start|restart) service="\${2:-}" ;;
|
||||
enable)
|
||||
for argument in "\$@"; do
|
||||
[[ "\$argument" != x-ui && "\$argument" != x-ui.service ]] || service=x-ui
|
||||
done
|
||||
;;
|
||||
esac
|
||||
if [[ ( "\$service" == x-ui || "\$service" == x-ui.service ) && ! -e "\$RELEASE_MARKER" ]]; then
|
||||
[[ -x "\$XUI_BINARY" ]] || { printf '[x] x-ui binary missing before guarded service start\\n' >&2; exit 1; }
|
||||
"\$XUI_BINARY" setting -listenIP 127.0.0.1 \\
|
||||
|| { printf '[x] Refusing x-ui start: loopback bind could not be applied\\n' >&2; exit 1; }
|
||||
[[ -f "\$XUI_DB" && ! -L "\$XUI_DB" ]] \\
|
||||
|| { printf '[x] Refusing x-ui start: settings database is unavailable\\n' >&2; exit 1; }
|
||||
"\$SQLITE3_BIN" "\$XUI_DB" <<'SQL' || { printf '[x] Refusing x-ui start: safe subscription bind could not be applied\\n' >&2; exit 1; }
|
||||
BEGIN IMMEDIATE;
|
||||
UPDATE settings SET value='127.0.0.1' WHERE key='subListen';
|
||||
INSERT INTO settings (key,value) SELECT 'subListen','127.0.0.1'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key='subListen');
|
||||
UPDATE settings SET value='false' WHERE key='subEnable';
|
||||
INSERT INTO settings (key,value) SELECT 'subEnable','false'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key='subEnable');
|
||||
COMMIT;
|
||||
SQL
|
||||
safe_sub_settings="\$("\$SQLITE3_BIN" "\$XUI_DB" "SELECT (SELECT value FROM settings WHERE key='subListen' LIMIT 1) || '|' || (SELECT value FROM settings WHERE key='subEnable' LIMIT 1);")"
|
||||
[[ "\$safe_sub_settings" == '127.0.0.1|false' ]] \\
|
||||
|| { printf '[x] Refusing x-ui start: safe subscription settings did not verify\\n' >&2; exit 1; }
|
||||
fi
|
||||
exec "\$REAL_SYSTEMCTL" "\$@"
|
||||
EOF
|
||||
chmod 700 "$directory/systemctl"
|
||||
|
||||
cat > "$directory/rc-service" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
REAL_RC_SERVICE=${rc_service_q}
|
||||
RELEASE_MARKER=${marker_q}
|
||||
XUI_BINARY=${binary_q}
|
||||
XUI_DB=${db_q}
|
||||
SQLITE3_BIN=${sqlite3_q}
|
||||
if [[ "\${1:-}" == x-ui && ( "\${2:-}" == start || "\${2:-}" == restart ) \\
|
||||
&& ! -e "\$RELEASE_MARKER" ]]; then
|
||||
[[ -x "\$XUI_BINARY" ]] || { printf '[x] x-ui binary missing before guarded service start\\n' >&2; exit 1; }
|
||||
"\$XUI_BINARY" setting -listenIP 127.0.0.1 \\
|
||||
|| { printf '[x] Refusing x-ui start: loopback bind could not be applied\\n' >&2; exit 1; }
|
||||
[[ -f "\$XUI_DB" && ! -L "\$XUI_DB" ]] \\
|
||||
|| { printf '[x] Refusing x-ui start: settings database is unavailable\\n' >&2; exit 1; }
|
||||
"\$SQLITE3_BIN" "\$XUI_DB" <<'SQL' || { printf '[x] Refusing x-ui start: safe subscription bind could not be applied\\n' >&2; exit 1; }
|
||||
BEGIN IMMEDIATE;
|
||||
UPDATE settings SET value='127.0.0.1' WHERE key='subListen';
|
||||
INSERT INTO settings (key,value) SELECT 'subListen','127.0.0.1'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key='subListen');
|
||||
UPDATE settings SET value='false' WHERE key='subEnable';
|
||||
INSERT INTO settings (key,value) SELECT 'subEnable','false'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key='subEnable');
|
||||
COMMIT;
|
||||
SQL
|
||||
safe_sub_settings="\$("\$SQLITE3_BIN" "\$XUI_DB" "SELECT (SELECT value FROM settings WHERE key='subListen' LIMIT 1) || '|' || (SELECT value FROM settings WHERE key='subEnable' LIMIT 1);")"
|
||||
[[ "\$safe_sub_settings" == '127.0.0.1|false' ]] \\
|
||||
|| { printf '[x] Refusing x-ui start: safe subscription settings did not verify\\n' >&2; exit 1; }
|
||||
fi
|
||||
[[ -x "\$REAL_RC_SERVICE" ]] || { printf '[x] rc-service is unavailable\\n' >&2; exit 127; }
|
||||
exec "\$REAL_RC_SERVICE" "\$@"
|
||||
EOF
|
||||
chmod 700 "$directory/rc-service"
|
||||
XUI_GUARD_DIR="$directory"
|
||||
XUI_BOOTSTRAP_RELEASE_MARKER="$marker"
|
||||
}
|
||||
|
||||
configure_xui_ports() {
|
||||
local backup_file panel_value subscription_value subscription_listeners
|
||||
|
||||
# XUI_WEB_LISTEN and XUI_SUB_LISTEN are changed to safe loopback values
|
||||
# during bootstrap. Recompute the requested final values from the explicit
|
||||
# access choices before entering that protected phase.
|
||||
set_xui_desired_listeners
|
||||
local backup_file panel_value subscription_value
|
||||
|
||||
[[ -x "$XUI_BINARY" ]] || die "После установки не найден $XUI_BINARY"
|
||||
[[ -f "$XUI_DB" && ! -L "$XUI_DB" ]] \
|
||||
|
|
@ -431,21 +189,16 @@ configure_xui_ports() {
|
|||
die "Не удалось изменить порт панели 3x-ui"
|
||||
fi
|
||||
|
||||
# The guarded first start always uses loopback. Public panel access is
|
||||
# enabled only after this protected listener has been observed.
|
||||
if ! set_xui_setting webListen 127.0.0.1 \
|
||||
|| ! set_xui_setting subPort "$SUBSCRIPTION_PORT" \
|
||||
|| ! set_xui_setting subListen 127.0.0.1 \
|
||||
|| ! set_xui_setting subPath "$XUI_SUB_PATH" \
|
||||
|| ! set_xui_setting subEnable false; then
|
||||
if ! set_xui_setting subPort "$SUBSCRIPTION_PORT" \
|
||||
|| ! set_xui_setting subEnable true; then
|
||||
cp -a -- "$backup_file" "$XUI_DB"
|
||||
systemctl start x-ui.service >/dev/null 2>&1 || true
|
||||
die "Не удалось изменить порт подписок 3x-ui; база восстановлена"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui.service
|
||||
systemctl start x-ui.service
|
||||
systemctl enable --now x-ui.service
|
||||
systemctl restart x-ui.service
|
||||
systemctl is-active --quiet x-ui.service || die "Служба x-ui не запущена"
|
||||
|
||||
panel_value="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='webPort' LIMIT 1;")"
|
||||
|
|
@ -454,41 +207,10 @@ configure_xui_ports() {
|
|||
|| die "В базе 3x-ui указан неожиданный порт панели: ${panel_value:-пусто}"
|
||||
[[ "$subscription_value" == "$SUBSCRIPTION_PORT" ]] \
|
||||
|| die "В базе 3x-ui указан неожиданный порт подписок: ${subscription_value:-пусто}"
|
||||
XUI_WEB_LISTEN="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='webListen' LIMIT 1;")"
|
||||
XUI_SUB_LISTEN="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='subListen' LIMIT 1;")"
|
||||
[[ "$XUI_WEB_LISTEN" == 127.0.0.1 ]] \
|
||||
|| die "Первый запуск 3x-ui остановлен: webListen не равен 127.0.0.1"
|
||||
[[ "$XUI_SUB_LISTEN" == 127.0.0.1 ]] \
|
||||
|| die "Первый запуск 3x-ui остановлен: subListen не равен 127.0.0.1"
|
||||
XUI_WEB_PATH="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='webBasePath' LIMIT 1;")"
|
||||
XUI_SUB_PATH="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='subPath' LIMIT 1;")"
|
||||
[[ "$XUI_WEB_PATH" == /* ]] || XUI_WEB_PATH="/${XUI_WEB_PATH}"
|
||||
[[ "$XUI_SUB_PATH" == /* ]] || XUI_SUB_PATH="/${XUI_SUB_PATH}"
|
||||
|
||||
wait_for_tcp_listener "$PANEL_PORT" "Панель 3x-ui"
|
||||
verify_private_listener "$PANEL_PORT" "Панель 3x-ui"
|
||||
subscription_listeners="$(ss -H -ltn "sport = :${SUBSCRIPTION_PORT}")"
|
||||
if [[ -n "$subscription_listeners" ]]; then
|
||||
verify_private_listener "$SUBSCRIPTION_PORT" "Сервис подписок 3x-ui"
|
||||
fi
|
||||
|
||||
systemctl stop x-ui.service
|
||||
if ! set_xui_setting webListen "$XUI_DESIRED_WEB_LISTEN" \
|
||||
|| ! set_xui_setting subListen "$XUI_DESIRED_SUB_LISTEN" \
|
||||
|| ! set_xui_setting subEnable "$XUI_DESIRED_SUB_ENABLED"; then
|
||||
die "Не удалось применить выбранный режим доступа 3x-ui"
|
||||
fi
|
||||
XUI_WEB_LISTEN="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='webListen' LIMIT 1;")"
|
||||
XUI_SUB_LISTEN="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='subListen' LIMIT 1;")"
|
||||
XUI_SUB_ENABLED="$(sqlite3 "$XUI_DB" "SELECT value FROM settings WHERE key='subEnable' LIMIT 1;")"
|
||||
[[ "$XUI_WEB_LISTEN" == "$XUI_DESIRED_WEB_LISTEN" ]] \
|
||||
|| die "Неожиданный конечный webListen в базе 3x-ui"
|
||||
[[ "$XUI_SUB_LISTEN" == "$XUI_DESIRED_SUB_LISTEN" ]] \
|
||||
|| die "Неожиданный конечный subListen в базе 3x-ui"
|
||||
[[ "$XUI_SUB_ENABLED" == "$XUI_DESIRED_SUB_ENABLED" ]] \
|
||||
|| die "Неожиданное конечное состояние сервиса подписок в базе 3x-ui"
|
||||
: > "$XUI_BOOTSTRAP_RELEASE_MARKER"
|
||||
systemctl start x-ui.service
|
||||
systemctl is-active --quiet x-ui.service || die "Служба x-ui не запустилась с выбранными настройками"
|
||||
}
|
||||
|
||||
install_cloudflare_warp() {
|
||||
|
|
@ -564,118 +286,80 @@ PANEL_PORT=""
|
|||
SUBSCRIPTION_PORT=""
|
||||
WARP_PORT=""
|
||||
XUI_PUBLIC_ACCESS=no
|
||||
XUI_SUBSCRIPTION_ACCESS=private
|
||||
XUI_WEB_LISTEN=127.0.0.1
|
||||
XUI_SUB_LISTEN=127.0.0.1
|
||||
XUI_WEB_PATH="/"
|
||||
XUI_SUB_PATH=""
|
||||
XUI_EXISTING_SUB_PATH=""
|
||||
XUI_SUB_PATH="/sub/"
|
||||
choose_port PANEL_PORT "Порт панели 3x-ui" "$DEFAULT_PANEL_PORT" "${forbidden_common[@]}"
|
||||
choose_port SUBSCRIPTION_PORT "Порт подписок 3x-ui" "$DEFAULT_SUBSCRIPTION_PORT" \
|
||||
"${forbidden_common[@]}" "$PANEL_PORT"
|
||||
choose_public_access
|
||||
choose_xui_source
|
||||
read -rp 'Установить Cloudflare WARP local proxy? [y/N]: ' WARP_ANSWER || WARP_ANSWER=no
|
||||
if [[ "$WARP_ANSWER" == y || "$WARP_ANSWER" == Y ]]; then
|
||||
choose_port WARP_PORT "Локальный proxy-порт WARP" "$DEFAULT_WARP_PORT" \
|
||||
choose_port WARP_PORT "Локальный proxy-порт WARP" "$DEFAULT_WARP_PORT" \
|
||||
"${forbidden_common[@]}" "$PANEL_PORT" "$SUBSCRIPTION_PORT"
|
||||
choose_public_access
|
||||
|
||||
printf '\nБудут использованы порты:\n'
|
||||
printf ' 3x-ui panel: %s/tcp\n' "$PANEL_PORT"
|
||||
printf ' 3x-ui subscriptions: %s/tcp\n' "$SUBSCRIPTION_PORT"
|
||||
printf ' WARP local proxy: 127.0.0.1:%s/tcp\n\n' "$WARP_PORT"
|
||||
if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then
|
||||
printf ' UFW: порты панели и подписок будут открыты публично\n\n'
|
||||
else
|
||||
WARP_PORT=""
|
||||
printf ' UFW: порты панели и подписок не будут открыты публично\n\n'
|
||||
fi
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
log "Установка системных зависимостей"
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg iproute2 openssl sqlite3 ufw
|
||||
for command_name in curl gpg openssl sqlite3 ss ufw; do
|
||||
apt-get install -y ca-certificates curl gnupg iproute2 sqlite3 ufw
|
||||
for command_name in curl gpg sqlite3 ss ufw; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
|
||||
load_existing_subscription_path
|
||||
choose_subscription_access
|
||||
|
||||
printf '\nБудут использованы порты:\n'
|
||||
printf ' 3x-ui panel: %s/tcp\n' "$PANEL_PORT"
|
||||
printf ' 3x-ui subscriptions: %s/tcp\n' "$SUBSCRIPTION_PORT"
|
||||
if [[ -n "$WARP_PORT" ]]; then printf ' WARP local proxy: 127.0.0.1:%s/tcp\n' "$WARP_PORT"; else printf ' WARP: выключен\n'; fi
|
||||
printf ' UFW: HTTPS 443 открыт; панель %s; подписки %s\n\n' \
|
||||
"$([[ "$XUI_PUBLIC_ACCESS" == yes ]] && printf 'публично' || printf 'закрыта')" \
|
||||
"$([[ "$XUI_SUBSCRIPTION_ACCESS" == public ]] && printf 'публично' || printf '%s' "$XUI_SUBSCRIPTION_ACCESS")"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
chmod 700 "$TMP_DIR"
|
||||
[[ -f "$SUPPLY_CHAIN_HELPER" && ! -L "$SUPPLY_CHAIN_HELPER" ]] \
|
||||
|| die "Не найден безопасный supply-chain helper: $SUPPLY_CHAIN_HELPER"
|
||||
if [[ "$XUI_SUPPLY_SOURCE" == github ]]; then
|
||||
bash "$SUPPLY_CHAIN_HELPER" prepare github "${TMP_DIR}/vendor"
|
||||
else
|
||||
bash "$SUPPLY_CHAIN_HELPER" prepare forgejo "${TMP_DIR}/vendor" "$XUI_SUPPLY_MIRROR"
|
||||
fi
|
||||
create_xui_start_guard "${TMP_DIR}/xui-start-guard" \
|
||||
"${TMP_DIR}/release-xui-listener" "$XUI_BINARY" "$XUI_DB"
|
||||
export PATH="${XUI_GUARD_DIR}:${PATH}"
|
||||
|
||||
log "Установка 3x-ui из проверенного релиза v3.10.2"
|
||||
bash "$SUPPLY_CHAIN_HELPER" install-xui "${TMP_DIR}/vendor"
|
||||
log "Загрузка официального установщика 3x-ui"
|
||||
XUI_INSTALLER="${TMP_DIR}/3x-ui-install.sh"
|
||||
curl --fail --silent --show-error --location \
|
||||
--proto '=https' --tlsv1.2 "$XUI_INSTALL_URL" -o "$XUI_INSTALLER"
|
||||
chmod 700 "$XUI_INSTALLER"
|
||||
bash -n "$XUI_INSTALLER"
|
||||
bash "$XUI_INSTALLER"
|
||||
|
||||
log "Настройка портов 3x-ui"
|
||||
configure_xui_ports
|
||||
|
||||
if [[ -n "$WARP_PORT" ]]; then
|
||||
log "Установка и настройка Cloudflare WARP"
|
||||
install_cloudflare_warp
|
||||
fi
|
||||
log "Установка и настройка Cloudflare WARP"
|
||||
install_cloudflare_warp
|
||||
|
||||
log "Настройка UFW"
|
||||
allow_ufw_port 80 "HTTP"
|
||||
allow_ufw_port 443 "HTTPS"
|
||||
if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then
|
||||
allow_ufw_port "$PANEL_PORT" "публичная панель 3x-ui"
|
||||
fi
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" == public ]]; then
|
||||
allow_ufw_port "$PANEL_PORT" "панель 3x-ui"
|
||||
allow_ufw_port "$SUBSCRIPTION_PORT" "подписки 3x-ui"
|
||||
else
|
||||
log "UFW: порт подписок оставлен закрытым для публичного Интернета"
|
||||
log "UFW: порты панели и подписок оставлены закрытыми для публичного Интернета"
|
||||
fi
|
||||
ufw --force enable
|
||||
|
||||
verify_ufw_port 443
|
||||
for port in 80 443; do
|
||||
verify_ufw_port "$port"
|
||||
done
|
||||
if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then
|
||||
verify_ufw_port "$PANEL_PORT"
|
||||
fi
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" == public ]]; then
|
||||
verify_ufw_port "$SUBSCRIPTION_PORT"
|
||||
for port in "$PANEL_PORT" "$SUBSCRIPTION_PORT"; do
|
||||
verify_ufw_port "$port"
|
||||
done
|
||||
fi
|
||||
|
||||
wait_for_tcp_listener "$PANEL_PORT" "Панель 3x-ui"
|
||||
if [[ "$XUI_PUBLIC_ACCESS" != yes ]]; then
|
||||
verify_private_listener "$PANEL_PORT" "Панель 3x-ui"
|
||||
fi
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" != disabled ]]; then
|
||||
wait_for_tcp_listener "$SUBSCRIPTION_PORT" "Сервис подписок 3x-ui"
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" != public ]]; then
|
||||
verify_private_listener "$SUBSCRIPTION_PORT" "Сервис подписок 3x-ui"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$WARP_PORT" ]]; then verify_warp_proxy; fi
|
||||
wait_for_tcp_listener "$SUBSCRIPTION_PORT" "Сервис подписок 3x-ui"
|
||||
verify_warp_proxy
|
||||
|
||||
printf '\nУстановка завершена.\n'
|
||||
if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then
|
||||
printf 'Панель 3x-ui: http(s)://<server-ip>:%s%s\n' "$PANEL_PORT" "$XUI_WEB_PATH"
|
||||
else
|
||||
printf 'Панель 3x-ui: http://127.0.0.1:%s%s (только локально)\n' "$PANEL_PORT" "$XUI_WEB_PATH"
|
||||
printf 'SSH tunnel: ssh -L %s:127.0.0.1:%s <user>@<server-ip>\n' "$PANEL_PORT" "$PANEL_PORT"
|
||||
fi
|
||||
if [[ "$XUI_SUBSCRIPTION_ACCESS" == disabled ]]; then
|
||||
printf 'Подписки 3x-ui: выключены\n'
|
||||
elif [[ "$XUI_SUBSCRIPTION_ACCESS" == public ]]; then
|
||||
printf 'Подписки 3x-ui: порт %s/tcp открыт; случайный путь сохранён в базе 3x-ui и не выведен в журнал\n' "$SUBSCRIPTION_PORT"
|
||||
else
|
||||
printf 'Подписки 3x-ui: http(s)://127.0.0.1:%s%s (только локально)\n' "$SUBSCRIPTION_PORT" "$XUI_SUB_PATH"
|
||||
fi
|
||||
if [[ -n "$WARP_PORT" ]]; then
|
||||
printf 'Локальный WARP proxy: socks5://127.0.0.1:%s\n' "$WARP_PORT"
|
||||
printf '\nПорт WARP не открыт публично в UFW: локальный proxy не требует аутентификации.\n'
|
||||
else
|
||||
printf 'Cloudflare WARP не устанавливался.\n'
|
||||
printf 'Панель 3x-ui: http(s)://<server-ip>:%s%s\n' "$PANEL_PORT" "$XUI_WEB_PATH"
|
||||
printf 'Подписки 3x-ui: http(s)://<server-ip>:%s%s\n' "$SUBSCRIPTION_PORT" "$XUI_SUB_PATH"
|
||||
printf 'Локальный WARP proxy: socks5://127.0.0.1:%s\n' "$WARP_PORT"
|
||||
printf '\nПорт WARP не открыт публично в UFW: локальный proxy не требует аутентификации.\n'
|
||||
if [[ "$XUI_PUBLIC_ACCESS" != yes ]]; then
|
||||
printf 'Порты панели и подписок не открыты публично. Используйте SSH tunnel, management VPN или повторный запуск с явным публичным режимом.\n'
|
||||
fi
|
||||
printf 'Настройте TLS, сложный путь панели, новые учётные данные и двухфакторную аутентификацию в 3x-ui.\n'
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Reviewed upstream release and SHA-256 pins. Supported architectures: amd64, arm64.
|
||||
# Upstream installer repositories carry GPL-3.0 licenses; these pins fix exact bytes.
|
||||
3x-ui-tag=v3.10.2
|
||||
3x-ui-installer-sha256=9684bae4f769b2127fcceceb4f200045657381311867529e6ba76c6b58e2825d
|
||||
3x-ui-amd64-sha256=bfda7921984e7a72555c38d4e861c7a8b67b3686b658608937de99682f1452d0
|
||||
3x-ui-arm64-sha256=a867c65ce52ad49f6504dd4c9c395337dc92ba57ae478da4f1b7ddb624014ed5
|
||||
3x-ui-script-sha256=3c7dabe7ea9edf0d950eff1caffbf203a1f8b52e98fc75e203372757f47998c0
|
||||
3x-ui-debian-service-sha256=6e120b037ed0a1305331e31de21471d3e6485ea28133ccf85a45d1270258ff9e
|
||||
xray-install-commit=05aa47266c94e6fb5ca48cad96afdb4becb5891f
|
||||
xray-installer-sha256=65e9e2accfc0b3b5dd24e7f2c552f35af3e581068155163d66d40f1d04fb6c95
|
||||
xray-tag=v26.10.21
|
||||
xray-amd64-sha256=0eec3ed4163c273d8c8bf821f7d219e257921cf584aa3fb970715e458e631978
|
||||
xray-arm64-sha256=d1304946c486d98e6b886e9b931eaaaebb23835b30e217638037b7f2721d4e9c
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly XUI_TAG="v3.10.2"
|
||||
readonly XUI_INSTALLER_SHA="9684bae4f769b2127fcceceb4f200045657381311867529e6ba76c6b58e2825d"
|
||||
readonly XUI_SCRIPT_SHA="3c7dabe7ea9edf0d950eff1caffbf203a1f8b52e98fc75e203372757f47998c0"
|
||||
readonly XUI_SERVICE_SHA="6e120b037ed0a1305331e31de21471d3e6485ea28133ccf85a45d1270258ff9e"
|
||||
readonly XRAY_COMMIT="05aa47266c94e6fb5ca48cad96afdb4becb5891f"
|
||||
readonly XRAY_INSTALLER_SHA="65e9e2accfc0b3b5dd24e7f2c552f35af3e581068155163d66d40f1d04fb6c95"
|
||||
readonly XRAY_TAG="v26.10.21"
|
||||
|
||||
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
architecture() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|x64|amd64) printf 'amd64\n' ;;
|
||||
armv8*|armv8|arm64|aarch64) printf 'arm64\n' ;;
|
||||
*) die "Unsupported architecture for pinned installer payloads: $(uname -m)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
sha_for_arch() {
|
||||
case "$1:$2" in
|
||||
xui:amd64) printf '%s\n' bfda7921984e7a72555c38d4e861c7a8b67b3686b658608937de99682f1452d0 ;;
|
||||
xui:arm64) printf '%s\n' a867c65ce52ad49f6504dd4c9c395337dc92ba57ae478da4f1b7ddb624014ed5 ;;
|
||||
xray:amd64) printf '%s\n' 0eec3ed4163c273d8c8bf821f7d219e257921cf584aa3fb970715e458e631978 ;;
|
||||
xray:arm64) printf '%s\n' d1304946c486d98e6b886e9b931eaaaebb23835b30e217638037b7f2721d4e9c ;;
|
||||
*) die "Missing pinned digest for $1/$2" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
fetch_pinned() {
|
||||
local url="$1" dest="$2" expected_sha="$3" tmp
|
||||
tmp="${dest}.partial.$$"
|
||||
curl -fLsS --location --retry 2 --connect-timeout 15 --max-time 180 \
|
||||
--proto '=https' --tlsv1.2 -o "$tmp" -- "$url" \
|
||||
|| die "Pinned download failed: $(basename "$dest")"
|
||||
[[ -s "$tmp" && "$(sha256sum "$tmp" | awk '{print $1}')" == "$expected_sha" ]] \
|
||||
|| die "SHA-256 verification failed: $(basename "$dest")"
|
||||
chmod 600 "$tmp"
|
||||
mv -- "$tmp" "$dest"
|
||||
}
|
||||
|
||||
validate_prepared_dir() {
|
||||
local directory="$1"
|
||||
[[ -d "$directory" && ! -L "$directory" \
|
||||
&& "$(stat -c %u "$directory")" == 0 \
|
||||
&& "$(stat -c %a "$directory")" =~ ^[0-7]*[0-5][0-5]$ ]] \
|
||||
|| die "Prepared directory must be a root-owned directory not writable by group or others"
|
||||
}
|
||||
|
||||
validate_prepared_file() {
|
||||
local file="$1"
|
||||
[[ -f "$file" && ! -L "$file" && "$(stat -c %u "$file")" == 0 \
|
||||
&& "$(stat -c %a "$file")" =~ ^[0-7]*[0-5][0-5]$ ]] \
|
||||
|| die "Prepared file is missing or writable by group/others: $(basename "$file")"
|
||||
}
|
||||
|
||||
prepare() {
|
||||
local source="$1" destination="$2" mirror_base="${3:-}" arch xui_arch xray_arch
|
||||
local xui_sha xray_sha name expected
|
||||
arch="$(architecture)"
|
||||
case "$arch" in
|
||||
amd64) xui_arch=amd64; xray_arch=64 ;;
|
||||
arm64) xui_arch=arm64; xray_arch=arm64-v8a ;;
|
||||
esac
|
||||
|
||||
case "$source" in
|
||||
github)
|
||||
[[ -z "$mirror_base" ]] || die "Do not pass a mirror base with github source"
|
||||
;;
|
||||
forgejo)
|
||||
[[ "$mirror_base" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._/-]+)?$ \
|
||||
&& "$mirror_base" != *..* ]] \
|
||||
|| die "Forgejo mirror base must be a plain HTTPS URL without credentials, query, or traversal"
|
||||
mirror_base="${mirror_base%/}"
|
||||
;;
|
||||
*) die "Usage: $0 prepare {github|forgejo} NEW_DEST_DIR [FORGEJO_HTTPS_BASE]" ;;
|
||||
esac
|
||||
[[ "$source" != forgejo || -n "$mirror_base" ]] || die "Forgejo HTTPS base is required"
|
||||
[[ ! -e "$destination" && ! -L "$destination" ]] \
|
||||
|| die "Destination must not already exist: $destination"
|
||||
mkdir -m 700 -- "$destination"
|
||||
destination="$(readlink -f -- "$destination")"
|
||||
xui_sha="$(sha_for_arch xui "$arch")"
|
||||
xray_sha="$(sha_for_arch xray "$arch")"
|
||||
|
||||
if [[ "$source" == github ]]; then
|
||||
fetch_pinned "https://raw.githubusercontent.com/MHSanaei/3x-ui/${XUI_TAG}/install.sh" \
|
||||
"$destination/xui-install.sh" "$XUI_INSTALLER_SHA"
|
||||
fetch_pinned "https://github.com/MHSanaei/3x-ui/releases/download/${XUI_TAG}/x-ui-linux-${xui_arch}.tar.gz" \
|
||||
"$destination/x-ui-linux-${xui_arch}.tar.gz" "$xui_sha"
|
||||
fetch_pinned "https://raw.githubusercontent.com/MHSanaei/3x-ui/${XUI_TAG}/x-ui.sh" \
|
||||
"$destination/x-ui.sh" "$XUI_SCRIPT_SHA"
|
||||
fetch_pinned "https://raw.githubusercontent.com/MHSanaei/3x-ui/${XUI_TAG}/x-ui.service.debian" \
|
||||
"$destination/x-ui.service.debian" "$XUI_SERVICE_SHA"
|
||||
fetch_pinned "https://raw.githubusercontent.com/XTLS/Xray-install/${XRAY_COMMIT}/install-release.sh" \
|
||||
"$destination/xray-install.sh" "$XRAY_INSTALLER_SHA"
|
||||
fetch_pinned "https://github.com/XTLS/Xray-core/releases/download/${XRAY_TAG}/Xray-linux-${xray_arch}.zip" \
|
||||
"$destination/Xray-linux-${xray_arch}.zip" "$xray_sha"
|
||||
else
|
||||
for name in xui-install.sh "x-ui-linux-${xui_arch}.tar.gz" x-ui.sh \
|
||||
x-ui.service.debian xray-install.sh "Xray-linux-${xray_arch}.zip"; do
|
||||
case "$name" in
|
||||
xui-install.sh) expected="$XUI_INSTALLER_SHA" ;;
|
||||
x-ui-linux-*.tar.gz) expected="$xui_sha" ;;
|
||||
x-ui.sh) expected="$XUI_SCRIPT_SHA" ;;
|
||||
x-ui.service.debian) expected="$XUI_SERVICE_SHA" ;;
|
||||
xray-install.sh) expected="$XRAY_INSTALLER_SHA" ;;
|
||||
Xray-linux-*.zip) expected="$xray_sha" ;;
|
||||
esac
|
||||
fetch_pinned "${mirror_base}/${name}" "$destination/$name" "$expected"
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "$xui_sha" "x-ui-linux-${xui_arch}.tar.gz" \
|
||||
> "$destination/x-ui-linux-${xui_arch}.tar.gz.sha256"
|
||||
printf 'SHA2-256= %s\n' "$xray_sha" \
|
||||
> "$destination/Xray-linux-${xray_arch}.zip.dgst"
|
||||
chmod 600 "$destination"/*
|
||||
printf '[+] Verified pinned installers and release assets in %s\n' "$destination"
|
||||
}
|
||||
|
||||
prepare_xray() {
|
||||
local source="$1" destination="$2" mirror_base="${3:-}" arch xray_arch xray_sha
|
||||
[[ ! -e "$destination" && ! -L "$destination" ]] \
|
||||
|| die "Destination must not already exist: $destination"
|
||||
arch="$(architecture)"
|
||||
case "$arch" in
|
||||
amd64) xray_arch=64 ;;
|
||||
arm64) xray_arch=arm64-v8a ;;
|
||||
esac
|
||||
xray_sha="$(sha_for_arch xray "$arch")"
|
||||
case "$source" in
|
||||
github)
|
||||
[[ -z "$mirror_base" ]] || die "Do not pass a mirror base with github source"
|
||||
;;
|
||||
forgejo)
|
||||
[[ "$mirror_base" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._/-]+)?$ \
|
||||
&& "$mirror_base" != *..* ]] \
|
||||
|| die "Forgejo mirror base must be a plain HTTPS URL without credentials, query, or traversal"
|
||||
mirror_base="${mirror_base%/}"
|
||||
;;
|
||||
*) die "Usage: $0 prepare-xray {github|forgejo} NEW_DEST_DIR [FORGEJO_HTTPS_BASE]" ;;
|
||||
esac
|
||||
[[ "$source" != forgejo || -n "$mirror_base" ]] || die "Forgejo HTTPS base is required"
|
||||
mkdir -m 700 -- "$destination"
|
||||
destination="$(readlink -f -- "$destination")"
|
||||
if [[ "$source" == github ]]; then
|
||||
fetch_pinned "https://raw.githubusercontent.com/XTLS/Xray-install/${XRAY_COMMIT}/install-release.sh" \
|
||||
"$destination/xray-install.sh" "$XRAY_INSTALLER_SHA"
|
||||
fetch_pinned "https://github.com/XTLS/Xray-core/releases/download/${XRAY_TAG}/Xray-linux-${xray_arch}.zip" \
|
||||
"$destination/Xray-linux-${xray_arch}.zip" "$xray_sha"
|
||||
else
|
||||
fetch_pinned "${mirror_base}/xray-install.sh" "$destination/xray-install.sh" "$XRAY_INSTALLER_SHA"
|
||||
fetch_pinned "${mirror_base}/Xray-linux-${xray_arch}.zip" \
|
||||
"$destination/Xray-linux-${xray_arch}.zip" "$xray_sha"
|
||||
fi
|
||||
printf 'SHA2-256= %s\n' "$xray_sha" \
|
||||
> "$destination/Xray-linux-${xray_arch}.zip.dgst"
|
||||
chmod 600 "$destination"/*
|
||||
printf '[+] Verified pinned Xray %s installer and payload in %s\n' "$XRAY_TAG" "$destination"
|
||||
}
|
||||
|
||||
write_xui_curl_shim() {
|
||||
local directory="$1" shim_dir="$2"
|
||||
mkdir -m 700 -p -- "$shim_dir"
|
||||
cat > "$shim_dir/curl" <<'CURL_SHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
url=""
|
||||
out=""
|
||||
has_writeout=no
|
||||
previous=""
|
||||
for argument in "$@"; do
|
||||
if [[ "$previous" == -o || "$previous" == --output \
|
||||
|| "$previous" =~ ^-[A-Za-z]*o$ ]]; then out="$argument"; fi
|
||||
[[ "$argument" != -w && "$argument" != --write-out ]] || has_writeout=yes
|
||||
[[ "$argument" != http://* && "$argument" != https://* ]] || url="$argument"
|
||||
previous="$argument"
|
||||
done
|
||||
answer_file=""
|
||||
case "$url" in
|
||||
https://github.com/MHSanaei/3x-ui/releases/download/v3.10.2/x-ui-linux-*.tar.gz)
|
||||
answer_file="${SUPPLY_DIR}/$(basename "$url")" ;;
|
||||
https://github.com/MHSanaei/3x-ui/releases/download/v3.10.2/x-ui-linux-*.tar.gz.sha256)
|
||||
answer_file="${SUPPLY_DIR}/$(basename "$url")" ;;
|
||||
https://raw.githubusercontent.com/MHSanaei/3x-ui/v3.10.2/x-ui.sh)
|
||||
answer_file="${SUPPLY_DIR}/x-ui.sh" ;;
|
||||
https://raw.githubusercontent.com/MHSanaei/3x-ui/v3.10.2/x-ui.service.debian)
|
||||
answer_file="${SUPPLY_DIR}/x-ui.service.debian" ;;
|
||||
https://raw.githubusercontent.com/MHSanaei/3x-ui/v3.10.2/x-ui.rc)
|
||||
printf 'Unsupported platform data request\n' >&2; exit 1 ;;
|
||||
https://check-host.net/ip)
|
||||
[[ -x "${REAL_CURL_BIN:-}" ]] || { printf 'curl is unavailable for pinned data lookup\n' >&2; exit 1; }
|
||||
exec "$REAL_CURL_BIN" "$@" --proto '=https' --tlsv1.2 --max-time 5 ;;
|
||||
*) printf 'Blocked unpinned 3x-ui installer request: %s\n' "$url" >&2; exit 1 ;;
|
||||
esac
|
||||
[[ -r "$answer_file" ]] || { printf 'Pinned data file is missing: %s\n' "$answer_file" >&2; exit 1; }
|
||||
if [[ -n "$out" && "$out" != /dev/null ]]; then
|
||||
cp -- "$answer_file" "$out"
|
||||
fi
|
||||
if [[ "$has_writeout" == yes ]]; then printf '200'; fi
|
||||
CURL_SHIM
|
||||
chmod 700 "$shim_dir/curl"
|
||||
: "$directory"
|
||||
}
|
||||
|
||||
install_xui() {
|
||||
local directory="$1" arch xui_arch shim_dir
|
||||
directory="$(readlink -f -- "$directory")"
|
||||
validate_prepared_dir "$directory"
|
||||
arch="$(architecture)"
|
||||
case "$arch" in amd64) xui_arch=amd64 ;; arm64) xui_arch=arm64 ;; esac
|
||||
for file in xui-install.sh "x-ui-linux-${xui_arch}.tar.gz" \
|
||||
"x-ui-linux-${xui_arch}.tar.gz.sha256" x-ui.sh x-ui.service.debian; do
|
||||
validate_prepared_file "$directory/$file"
|
||||
done
|
||||
[[ "$(sha256sum "$directory/xui-install.sh" | awk '{print $1}')" == "$XUI_INSTALLER_SHA" \
|
||||
&& "$(sha256sum "$directory/x-ui.sh" | awk '{print $1}')" == "$XUI_SCRIPT_SHA" \
|
||||
&& "$(sha256sum "$directory/x-ui.service.debian" | awk '{print $1}')" == "$XUI_SERVICE_SHA" \
|
||||
&& "$(sha256sum "$directory/x-ui-linux-${xui_arch}.tar.gz" | awk '{print $1}')" == "$(sha_for_arch xui "$arch")" ]] \
|
||||
|| die "Prepared 3x-ui payload no longer matches the pins"
|
||||
shim_dir="$(mktemp -d /tmp/ochenstarik-xui-curl.XXXXXX)"
|
||||
trap 'rm -rf -- "$shim_dir"' EXIT
|
||||
local real_curl
|
||||
real_curl="$(command -v curl)"
|
||||
[[ -x "$real_curl" ]] || die "curl is required for the pinned data lookup"
|
||||
write_xui_curl_shim "$directory" "$shim_dir"
|
||||
(cd -- "$directory" && SUPPLY_DIR="$directory" REAL_CURL_BIN="$real_curl" PATH="$shim_dir:$PATH" \
|
||||
XUI_NONINTERACTIVE=1 bash ./xui-install.sh "$XUI_TAG")
|
||||
rm -rf -- "$shim_dir"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
install_xray() {
|
||||
local directory="$1" arch xray_arch expected
|
||||
directory="$(readlink -f -- "$directory")"
|
||||
validate_prepared_dir "$directory"
|
||||
arch="$(architecture)"
|
||||
case "$arch" in amd64) xray_arch=64 ;; arm64) xray_arch=arm64-v8a ;; esac
|
||||
expected="$(sha_for_arch xray "$arch")"
|
||||
validate_prepared_file "$directory/xray-install.sh"
|
||||
validate_prepared_file "$directory/Xray-linux-${xray_arch}.zip"
|
||||
validate_prepared_file "$directory/Xray-linux-${xray_arch}.zip.dgst"
|
||||
[[ "$(sha256sum "$directory/xray-install.sh" | awk '{print $1}')" == "$XRAY_INSTALLER_SHA" \
|
||||
&& "$(sha256sum "$directory/Xray-linux-${xray_arch}.zip" | awk '{print $1}')" == "$expected" \
|
||||
&& "$(awk -F '= ' '/^SHA2-256=/ {print $2}' "$directory/Xray-linux-${xray_arch}.zip.dgst")" == "$expected" ]] \
|
||||
|| die "Prepared Xray payload no longer matches the pins"
|
||||
(cd -- "$directory" && bash ./xray-install.sh install --local "Xray-linux-${xray_arch}.zip")
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
prepare)
|
||||
[[ $# -ge 3 && $# -le 4 ]] || die "Usage: $0 prepare {github|forgejo} NEW_DEST_DIR [FORGEJO_HTTPS_BASE]"
|
||||
prepare "$2" "$3" "${4:-}"
|
||||
;;
|
||||
prepare-xray)
|
||||
[[ $# -ge 3 && $# -le 4 ]] || die "Usage: $0 prepare-xray {github|forgejo} NEW_DEST_DIR [FORGEJO_HTTPS_BASE]"
|
||||
prepare_xray "$2" "$3" "${4:-}"
|
||||
;;
|
||||
install-xui)
|
||||
[[ $# == 2 ]] || die "Usage: $0 install-xui PREPARED_DIR"
|
||||
install_xui "$2"
|
||||
;;
|
||||
install-xray)
|
||||
[[ $# == 2 ]] || die "Usage: $0 install-xray PREPARED_DIR"
|
||||
install_xray "$2"
|
||||
;;
|
||||
*) die "Usage: $0 {prepare|prepare-xray|install-xui|install-xray} ..." ;;
|
||||
esac
|
||||
|
|
@ -56,7 +56,7 @@ send_message() {
|
|||
}
|
||||
|
||||
[[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root"
|
||||
for command_name in curl sshd setsid; do
|
||||
for command_name in curl sshd; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || die "Не найдена команда: $command_name; сначала запустите второй скрипт"
|
||||
done
|
||||
[[ -f "$PAM_SSHD" && ! -L "$PAM_SSHD" ]] || die "Не найден файл конфигурации PAM для SSH: $PAM_SSHD"
|
||||
|
|
@ -108,12 +108,6 @@ set -Euo pipefail
|
|||
|
||||
readonly CONFIG_FILE="/etc/ochenstarik-server/telegram.conf"
|
||||
readonly LOG_FILE="/var/log/ochenstarik-ssh-login-telegram.log"
|
||||
[[ "${PAM_TYPE:-}" == open_session ]] || exit 0
|
||||
if [[ "${1:-}" != --worker ]]; then
|
||||
/usr/bin/setsid --fork "$0" --worker \
|
||||
</dev/null >/dev/null 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
[[ -r "$CONFIG_FILE" ]] || exit 0
|
||||
umask 077
|
||||
|
||||
|
|
|
|||
|
|
@ -73,12 +73,6 @@ is_valid_port() {
|
|||
(( 10#$port >= 1 && 10#$port <= 65535 ))
|
||||
}
|
||||
|
||||
prepare_sshd_runtime_directory() {
|
||||
# Socket-activated Ubuntu can have no active ssh.service after an upgrade.
|
||||
# Its RuntimeDirectory then does not exist, but standalone sshd -t needs it.
|
||||
install -d -o root -g root -m 755 /run/sshd
|
||||
}
|
||||
|
||||
read_ip_mode() {
|
||||
local mode=both
|
||||
if [[ -e "$IP_FAMILY_CONFIG" ]]; then
|
||||
|
|
@ -113,52 +107,6 @@ allow_ufw_rule() {
|
|||
fi
|
||||
}
|
||||
|
||||
delete_ufw_rule_matches() {
|
||||
local attempt
|
||||
for attempt in {1..5}; do
|
||||
ufw --force delete allow "$@" >/dev/null 2>&1 || break
|
||||
done
|
||||
}
|
||||
|
||||
retire_temporary_ssh_port_22() {
|
||||
local managed_file="${1:-$MANAGED_PORTS_CONFIG}" answer temp_file ufw_status
|
||||
|
||||
[[ "$MANAGE_UFW" == yes && "$SSH_PORT" != 22 ]] || return 0
|
||||
read -rp 'Успешно проверили вход по новому SSH-порту в отдельном терминале? Удалить временный порт 22? [y/N]: ' answer \
|
||||
|| answer=""
|
||||
[[ "$answer" == y || "$answer" == Y ]] || {
|
||||
log "Правило порта 22 оставлено для безопасного доступа"
|
||||
return 0
|
||||
}
|
||||
|
||||
require_command ufw
|
||||
delete_ufw_rule_matches 22/tcp
|
||||
delete_ufw_rule_matches from 0.0.0.0/0 to any port 22 proto tcp
|
||||
delete_ufw_rule_matches from ::/0 to any port 22 proto tcp
|
||||
|
||||
ufw_status="$(LANG=C ufw status)" \
|
||||
|| die "Could not verify UFW after deleting temporary SSH port 22; keeping its managed record"
|
||||
if grep -Eq '^22(/tcp)?([[:space:]]+\(v6\))?[[:space:]]+ALLOW([[:space:]]|$)' <<< "$ufw_status"; then
|
||||
die "UFW still allows temporary SSH port 22; keeping its managed record"
|
||||
fi
|
||||
|
||||
if [[ -e "$managed_file" ]]; then
|
||||
[[ -f "$managed_file" && ! -L "$managed_file" ]] \
|
||||
|| die "$managed_file must be a regular non-symlink file"
|
||||
temp_file="$(mktemp "${managed_file}.XXXXXX")"
|
||||
if grep -Fvx '22/tcp' "$managed_file" > "$temp_file"; then
|
||||
:
|
||||
elif [[ "$?" -ne 1 ]]; then
|
||||
rm -f -- "$temp_file"
|
||||
die "Could not update $managed_file"
|
||||
fi
|
||||
chown root:root "$temp_file"
|
||||
chmod 600 "$temp_file"
|
||||
mv -f -- "$temp_file" "$managed_file"
|
||||
fi
|
||||
log "Временный порт 22 удалён из UFW и списка управляемых правил"
|
||||
}
|
||||
|
||||
select_action() {
|
||||
local choice
|
||||
|
||||
|
|
@ -523,7 +471,6 @@ UsePAM yes
|
|||
EOF
|
||||
chmod 644 "$SSHD_DROPIN"
|
||||
|
||||
prepare_sshd_runtime_directory
|
||||
sshd -t || die "sshd syntax validation failed"
|
||||
# Capture the complete output once. With pipefail enabled, piping `sshd -T`
|
||||
# into an awk program that exits after the first match can terminate sshd with
|
||||
|
|
@ -541,6 +488,7 @@ if [[ "$MANAGE_UFW" == yes ]]; then
|
|||
log "Allowing SSH, HTTPS, and application ports in UFW without resetting existing rules"
|
||||
allow_ufw_rule "${SSH_PORT}/tcp"
|
||||
allow_ufw_rule "443/tcp"
|
||||
allow_ufw_rule "63636/tcp"
|
||||
ufw --force enable
|
||||
else
|
||||
warn "MANAGE_UFW=no: firewall was not enabled or modified"
|
||||
|
|
@ -583,5 +531,19 @@ printf 'Password SSH authentication: %s\n' "$PASSWORD_AUTH"
|
|||
printf 'Passwordless sudo: %s\n' "$PASSWORDLESS_SUDO"
|
||||
printf '\nOpen a NEW terminal and test before closing the current root session:\n'
|
||||
printf 'ssh -p %s %s@<server-ip>\n' "$SSH_PORT" "$NEW_USERNAME"
|
||||
retire_temporary_ssh_port_22
|
||||
case "$(read_ip_mode)" in
|
||||
ipv4)
|
||||
printf 'After that login succeeds, remove temporary port 22 with:\n'
|
||||
printf 'sudo ufw delete allow from 0.0.0.0/0 to any port 22 proto tcp\n'
|
||||
;;
|
||||
ipv6)
|
||||
printf 'After that login succeeds, remove temporary port 22 with:\n'
|
||||
printf 'sudo ufw delete allow from ::/0 to any port 22 proto tcp\n'
|
||||
;;
|
||||
both)
|
||||
printf 'After that login succeeds, remove both temporary port 22 rules:\n'
|
||||
printf 'sudo ufw delete allow from 0.0.0.0/0 to any port 22 proto tcp\n'
|
||||
printf 'sudo ufw delete allow from ::/0 to any port 22 proto tcp\n'
|
||||
;;
|
||||
esac
|
||||
printf 'To enable Telegram login notifications, run ochenstarik-server-tg-4.sh.\n'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly XRAY_CONFIG="/usr/local/etc/xray/config.json"
|
||||
readonly XRAY_DIR="/usr/local/etc/xray"
|
||||
readonly STATE_DIR="/etc/ochenstarik-xray"
|
||||
|
|
@ -15,45 +14,6 @@ log() { printf '[+] %s\n' "$*"; }
|
|||
warn() { printf '[!] %s\n' "$*" >&2; }
|
||||
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
choose_xray_source() {
|
||||
local choice mirror_base
|
||||
printf '\nИсточник установщика Xray и релиза v26.10.21:\n'
|
||||
printf ' 1) GitHub (по умолчанию)\n'
|
||||
printf ' 2) HTTPS-зеркало Forgejo\n'
|
||||
while :; do
|
||||
read -rp 'Выберите источник [1]: ' choice || die "Ввод прерван"
|
||||
choice="${choice:-1}"
|
||||
case "$choice" in
|
||||
1) XRAY_SOURCE=github; XRAY_MIRROR_BASE=""; return 0 ;;
|
||||
2)
|
||||
read -rp 'Базовый HTTPS URL зеркала с проверенными файлами: ' mirror_base \
|
||||
|| die "Ввод прерван"
|
||||
XRAY_SOURCE=forgejo
|
||||
XRAY_MIRROR_BASE="$mirror_base"
|
||||
return 0
|
||||
;;
|
||||
*) warn "Введите 1 или 2" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
fetch_subscription() {
|
||||
local url="$1" output_file="$2" escaped_url LC_ALL=C
|
||||
# curl's config parser accepts C-style escaping for backslash and quote.
|
||||
# Reject control characters so the operator cannot add a second directive.
|
||||
if [[ "$url" == *[[:cntrl:]]* ]]; then
|
||||
die "Ссылка подписки содержит управляющие символы"
|
||||
fi
|
||||
escaped_url="${url//\\/\\\\}"
|
||||
escaped_url="${escaped_url//\"/\\\"}"
|
||||
{
|
||||
printf 'url = "%s"\n' "$escaped_url"
|
||||
printf 'fail\nsilent\nshow-error\nlocation\n'
|
||||
printf 'proto = "=https"\ntlsv1.2\nmax-time = 30\n'
|
||||
} | curl --config - -o "$output_file" \
|
||||
|| die "Не удалось загрузить подписку"
|
||||
}
|
||||
|
||||
TMP_DIR=""
|
||||
cleanup() {
|
||||
[[ -z "$TMP_DIR" ]] || rm -rf -- "$TMP_DIR"
|
||||
|
|
@ -215,11 +175,17 @@ LINKS_FILE="${TMP_DIR}/vless-links.txt"
|
|||
LINK_FILE="${TMP_DIR}/vless-link.txt"
|
||||
PORTS_FILE="${TMP_DIR}/vless-ports.txt"
|
||||
GENERATED_CONFIG="${TMP_DIR}/config.json"
|
||||
INSTALLER_FILE="${TMP_DIR}/install-release.sh"
|
||||
|
||||
if [[ "$SUBSCRIPTION_INPUT" == vless://* ]]; then
|
||||
printf '%s' "$SUBSCRIPTION_INPUT" > "$RAW_FILE"
|
||||
else
|
||||
fetch_subscription "$SUBSCRIPTION_INPUT" "$RAW_FILE"
|
||||
{
|
||||
printf 'url = "%s"\n' "$SUBSCRIPTION_INPUT"
|
||||
printf 'fail\nsilent\nshow-error\nlocation\n'
|
||||
printf 'proto = "=https"\ntlsv1.2\nmax-time = 30\n'
|
||||
} | curl --config - > "$RAW_FILE" \
|
||||
|| die "Не удалось загрузить подписку"
|
||||
fi
|
||||
unset SUBSCRIPTION_INPUT
|
||||
chmod 600 "$RAW_FILE"
|
||||
|
|
@ -432,21 +398,13 @@ valid_ipv4 "$VPN_IP" || die "Получен некорректный IPv4 VPN-с
|
|||
chmod 600 "$GENERATED_CONFIG"
|
||||
log "Подписка разобрана: VLESS + TCP/RAW + REALITY"
|
||||
|
||||
log "Установка Xray v26.10.21 с проверкой закреплённых SHA-256"
|
||||
choose_xray_source
|
||||
SUPPLY_CHAIN_HELPER="${SCRIPT_DIR}/ochenstarik-server-supply-chain.sh"
|
||||
[[ -f "$SUPPLY_CHAIN_HELPER" && ! -L "$SUPPLY_CHAIN_HELPER" ]] \
|
||||
|| die "Не найден локальный проверяющий загрузки: $SUPPLY_CHAIN_HELPER"
|
||||
XRAY_SUPPLY_DIR="${TMP_DIR}/xray-supply-chain"
|
||||
if [[ "$XRAY_SOURCE" == github ]]; then
|
||||
bash "$SUPPLY_CHAIN_HELPER" prepare-xray github "$XRAY_SUPPLY_DIR" \
|
||||
|| die "Не удалось проверить файлы Xray из GitHub"
|
||||
else
|
||||
bash "$SUPPLY_CHAIN_HELPER" prepare-xray forgejo "$XRAY_SUPPLY_DIR" "$XRAY_MIRROR_BASE" \
|
||||
|| die "Не удалось проверить файлы Xray из Forgejo-зеркала"
|
||||
fi
|
||||
bash "$SUPPLY_CHAIN_HELPER" install-xray "$XRAY_SUPPLY_DIR" \
|
||||
|| die "Установка закреплённого Xray завершилась ошибкой"
|
||||
log "Установка или обновление Xray из официального репозитория XTLS"
|
||||
curl --fail --silent --show-error --location \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh \
|
||||
-o "$INSTALLER_FILE"
|
||||
chmod 700 "$INSTALLER_FILE"
|
||||
bash "$INSTALLER_FILE" install
|
||||
|
||||
command -v xray >/dev/null 2>&1 || die "Xray не установлен"
|
||||
install -d -m 755 "$XRAY_DIR" /var/log/xray
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$tmp"' EXIT
|
||||
|
||||
awk '/^ cat > "\$BACKUP_RUNNER" <<.BACKUP_RUNNER_EOF/ { copying=1; next }
|
||||
copying && /^BACKUP_RUNNER_EOF$/ { exit }
|
||||
copying { print }' ochenstarik-server-backup-7.sh > "$tmp/runner"
|
||||
awk '/^\[\[ "\$EUID"/ { exit } { print }' "$tmp/runner" > "$tmp/runner-functions"
|
||||
bash -n "$tmp/runner-functions"
|
||||
source "$tmp/runner-functions"
|
||||
|
||||
# Exercise the real create_backup early-return path with only the SSH transport
|
||||
# replaced. This never connects to a host or reads the real filesystem.
|
||||
protect_initial_files() { :; }
|
||||
mkdir -p "$tmp/bin"
|
||||
cat > "$tmp/bin/scp" <<'SCP_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "$*" >> "$SCP_CALLS"
|
||||
exit "${SCP_EXIT_CODE:-0}"
|
||||
SCP_STUB
|
||||
chmod +x "$tmp/bin/scp"
|
||||
export PATH="$tmp/bin:$PATH" SCP_CALLS="$tmp/scp-calls"
|
||||
|
||||
BACKUP_ROOT="$tmp/encrypted-root"
|
||||
BACKUP_ENCRYPTION=age
|
||||
AGE_RECIPIENT='age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'
|
||||
OFFSITE_TARGET='backup@example.test'
|
||||
OFFSITE_PATH='/srv/backups'
|
||||
OFFSITE_KNOWN_HOSTS="$tmp/known_hosts"
|
||||
mkdir -p "$BACKUP_ROOT/initial"
|
||||
touch "$OFFSITE_KNOWN_HOSTS"
|
||||
archive="$BACKUP_ROOT/initial/initial-20261023-120000.tar.zst.age"
|
||||
printf 'ciphertext fixture\n' > "$archive"
|
||||
(
|
||||
cd "$BACKUP_ROOT/initial"
|
||||
sha256sum "$(basename "$archive")" > "$(basename "$archive").sha256"
|
||||
)
|
||||
checksum="${archive}.sha256"
|
||||
|
||||
# A failed first offsite upload must leave both immutable local files intact,
|
||||
# return failure, and retry those exact files on the next invocation.
|
||||
if (SCP_EXIT_CODE=42 create_backup initial 0) > "$tmp/failed.out" 2>&1; then
|
||||
printf 'Initial offsite failure was accepted as success.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq 'Первичный снимок уже существует' "$tmp/failed.out"; then
|
||||
printf 'Failed upload was reported as a successful existing backup.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ -f "$archive" && -f "$checksum" ]]
|
||||
(
|
||||
cd "$BACKUP_ROOT/initial"
|
||||
sha256sum --check --status "$(basename "$checksum")"
|
||||
)
|
||||
[[ "$(wc -l < "$SCP_CALLS")" == 1 ]]
|
||||
grep -Fq -- "$archive" "$SCP_CALLS"
|
||||
grep -Fq -- "$checksum" "$SCP_CALLS"
|
||||
|
||||
create_backup initial 0 > "$tmp/retry.out"
|
||||
grep -Fq 'Первичный снимок уже существует' "$tmp/retry.out"
|
||||
[[ "$(wc -l < "$SCP_CALLS")" == 2 ]]
|
||||
[[ "$(sha256sum "$archive" | awk '{print $1}')" == \
|
||||
"$(cd "$BACKUP_ROOT/initial" && sha256sum --check --status "$(basename "$checksum")" && sha256sum "$(basename "$archive")" | awk '{print $1}')" ]]
|
||||
|
||||
# Selecting age cannot silently accept an existing plaintext initial snapshot.
|
||||
plain_root="$tmp/plain-root"
|
||||
mkdir -p "$plain_root/initial"
|
||||
plain_archive="$plain_root/initial/initial-20261022-120000.tar.zst"
|
||||
printf 'plaintext fixture\n' > "$plain_archive"
|
||||
(
|
||||
cd "$plain_root/initial"
|
||||
sha256sum "$(basename "$plain_archive")" > "$(basename "$plain_archive").sha256"
|
||||
)
|
||||
BACKUP_ROOT="$plain_root"
|
||||
if (create_backup initial 0) > "$tmp/plain.out" 2>&1; then
|
||||
printf 'Age policy silently accepted an existing plaintext snapshot.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fq 'уже создан без шифрования' "$tmp/plain.out"
|
||||
[[ -f "$plain_archive" && "$(wc -l < "$SCP_CALLS")" == 2 ]]
|
||||
|
||||
printf 'Initial backup retry and encryption-policy tests passed.\n'
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$tmp"' EXIT
|
||||
mkdir -p "$tmp/bin"
|
||||
|
||||
# install-xui allocates the shim with mktemp -d before populating it. Exercise
|
||||
# that real lifecycle, then both the cached and rejected download paths.
|
||||
source <(awk '/^write_xui_curl_shim\(\)/ { copying=1 } copying { print } copying && /^}/ { exit }' \
|
||||
ochenstarik-server-supply-chain.sh)
|
||||
mkdir -m 700 "$tmp/shim" "$tmp/vendor"
|
||||
printf 'verified-fixture\n' > "$tmp/vendor/x-ui.sh"
|
||||
write_xui_curl_shim "$tmp/vendor" "$tmp/shim"
|
||||
SUPPLY_DIR="$tmp/vendor" "$tmp/shim/curl" -fLRo "$tmp/copied" \
|
||||
https://raw.githubusercontent.com/MHSanaei/3x-ui/v3.10.2/x-ui.sh
|
||||
cmp "$tmp/vendor/x-ui.sh" "$tmp/copied"
|
||||
if SUPPLY_DIR="$tmp/vendor" "$tmp/shim/curl" -o "$tmp/unknown" \
|
||||
https://untrusted.example.test/installer.sh >/dev/null 2>&1; then
|
||||
printf 'Shim accepted an unpinned installer request.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ ! -e "$tmp/unknown" ]]
|
||||
|
||||
# Load only the production URL fetcher so this test never calls the network or
|
||||
# runs the VPN installer.
|
||||
source <(awk '/^fetch_subscription\(\)/ { copying=1 } copying { print } copying && /^}/ { exit }' \
|
||||
ochenstarik-server-vpn-5.sh)
|
||||
die() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
source <(awk '/^choose_xray_source\(\)/ { copying=1 } copying { print } copying && /^}/ { exit }' \
|
||||
ochenstarik-server-vpn-5.sh)
|
||||
choose_xray_source <<< $'2\nhttps://git.example.test/project/releases/download/vendor-20261023'
|
||||
[[ "$XRAY_SOURCE" == forgejo \
|
||||
&& "$XRAY_MIRROR_BASE" == 'https://git.example.test/project/releases/download/vendor-20261023' ]]
|
||||
|
||||
cat > "$tmp/bin/curl" <<'CURL_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "$*" > "$CURL_ARGS_LOG"
|
||||
cat > "$CURL_CONFIG_LOG"
|
||||
while (($#)); do
|
||||
if [[ "$1" == -o ]]; then printf 'test payload\n' > "$2"; exit 0; fi
|
||||
shift
|
||||
done
|
||||
exit 1
|
||||
CURL_STUB
|
||||
chmod +x "$tmp/bin/curl"
|
||||
export PATH="$tmp/bin:$PATH"
|
||||
export CURL_ARGS_LOG="$tmp/curl-args" CURL_CONFIG_LOG="$tmp/curl-config"
|
||||
|
||||
literal_url='https://subscriptions.example.test/path/a"b\c?key=literal-marker'
|
||||
fetch_subscription "$literal_url" "$tmp/subscription"
|
||||
[[ "$(cat "$tmp/subscription")" == 'test payload' ]]
|
||||
grep -Fqx -- 'proto = "=https"' "$CURL_CONFIG_LOG"
|
||||
grep -Fqx -- 'tlsv1.2' "$CURL_CONFIG_LOG"
|
||||
grep -Fqx -- 'location' "$CURL_CONFIG_LOG"
|
||||
expected_url="${literal_url//\\/\\\\}"
|
||||
expected_url="${expected_url//\"/\\\"}"
|
||||
grep -Fqx -- "url = \"$expected_url\"" "$CURL_CONFIG_LOG"
|
||||
if grep -Fq 'literal-marker' "$CURL_ARGS_LOG"; then
|
||||
printf 'Subscription URL leaked into curl arguments.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (fetch_subscription $'https://subscriptions.example.test/good\nurl = "http://bad.example"' "$tmp/blocked") \
|
||||
>/dev/null 2>&1; then
|
||||
printf 'Control-character URL was accepted.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A large value must not bypass rejection through grep -q / SIGPIPE under
|
||||
# pipefail. Bash validates the entire value before curl sees any config.
|
||||
printf -v padding '%131072s' ''
|
||||
if (fetch_subscription $'https://subscriptions.example.test/\v'"${padding// /x}" "$tmp/blocked-large") \
|
||||
>/dev/null 2>&1; then
|
||||
printf 'Large control-character URL was accepted.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# S8 already carries these flags; lock the redirect policy in as a regression.
|
||||
grep -Fq -- "--proto '=https' --tlsv1.2" ochenstarik-server-ai-agents-8.sh
|
||||
|
||||
# A failed Forgejo mirror request must stop without trying GitHub.
|
||||
cat > "$tmp/bin/curl" <<'CURL_FAIL_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "$*" >> "$CURL_ATTEMPTS"
|
||||
exit 22
|
||||
CURL_FAIL_STUB
|
||||
chmod +x "$tmp/bin/curl"
|
||||
export CURL_ATTEMPTS="$tmp/attempts"
|
||||
if bash ./ochenstarik-server-supply-chain.sh prepare forgejo "$tmp/prepared" \
|
||||
'https://git.example.test/project/releases/download/vendor-20261023' \
|
||||
>/dev/null 2>&1; then
|
||||
printf 'Failed Forgejo fetch unexpectedly succeeded.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "$(wc -l < "$CURL_ATTEMPTS")" == 1 ]]
|
||||
grep -Fq 'https://git.example.test/project/releases/download/vendor-20261023/xui-install.sh' "$CURL_ATTEMPTS"
|
||||
if grep -Eq 'github\.com|raw\.githubusercontent\.com' "$CURL_ATTEMPTS"; then
|
||||
printf 'Forgejo failure fell back to GitHub.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if bash ./ochenstarik-server-supply-chain.sh prepare-xray forgejo "$tmp/xray-prepared" \
|
||||
'https://git.example.test/project/releases/download/vendor-20261023' \
|
||||
>/dev/null 2>&1; then
|
||||
printf 'Failed Forgejo Xray fetch unexpectedly succeeded.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "$(wc -l < "$CURL_ATTEMPTS")" == 2 ]]
|
||||
grep -Fq 'https://git.example.test/project/releases/download/vendor-20261023/xray-install.sh' "$CURL_ATTEMPTS"
|
||||
if grep -Eq 'github\.com|raw\.githubusercontent\.com' "$CURL_ATTEMPTS"; then
|
||||
printf 'Xray Forgejo failure fell back to GitHub.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Both installer steps must reach their pinned helper paths and avoid mutable
|
||||
# upstream main installer URLs.
|
||||
grep -Fq 'prepare-xray github' ochenstarik-server-vpn-5.sh
|
||||
grep -Fq 'prepare-xray forgejo' ochenstarik-server-vpn-5.sh
|
||||
grep -Fq 'install-xray "$XRAY_SUPPLY_DIR"' ochenstarik-server-vpn-5.sh
|
||||
grep -Fq 'prepare github' ochenstarik-server-panel-warp-6.sh
|
||||
grep -Fq 'prepare forgejo' ochenstarik-server-panel-warp-6.sh
|
||||
grep -Fq 'install-xui "${TMP_DIR}/vendor"' ochenstarik-server-panel-warp-6.sh
|
||||
if grep -Eq 'XTLS/Xray-install/main|MHSanaei/3x-ui/main' \
|
||||
ochenstarik-server-vpn-5.sh ochenstarik-server-panel-warp-6.sh; then
|
||||
printf 'An installer step still references a mutable upstream main URL.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Download security tests passed.\n'
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
|
||||
bash <<'STEP2_TEST'
|
||||
set -Eeuo pipefail
|
||||
source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-2.sh)
|
||||
SSH_PORT=20202
|
||||
SSH_CONNECTION='198.51.100.9 41000 192.0.2.1 22'
|
||||
MANAGED_RULES=()
|
||||
declare -A RULE_SEEN=()
|
||||
add_default_rules
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 20202/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 22/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 443/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 80/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 2096/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 40000/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 63636/tcp '* ]]
|
||||
|
||||
MANAGED_RULES=(22/tcp 20202/tcp 80/tcp 2096/tcp 443/tcp 40000/tcp 63636/tcp)
|
||||
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
|
||||
RULE_SEEN=()
|
||||
confirm_legacy_rules <<< 'n'
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 22/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 443/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 80/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 2096/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 40000/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 63636/tcp '* ]]
|
||||
legacy_delete_log="$(mktemp)"
|
||||
ufw() {
|
||||
if [[ "${1:-}" == status ]]; then
|
||||
printf '%s\n' "${UFW_TEST_STATUS:-$'Status: active\nTo Action From'}"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$*" >> "$legacy_delete_log"
|
||||
return 1
|
||||
}
|
||||
UFW_TEST_STATUS=$'Status: active\nTo Action From'
|
||||
delete_managed_ufw_rules
|
||||
for legacy_rule in 80/tcp 2096/tcp 40000/tcp 63636/tcp; do
|
||||
grep -Fq -- "--force delete allow ${legacy_rule}" "$legacy_delete_log"
|
||||
done
|
||||
UFW_TEST_STATUS=$'Status: active\nTo Action From\n80/tcp (v6) ALLOW IN Anywhere (v6)'
|
||||
if (delete_managed_ufw_rules) >/dev/null 2>&1; then
|
||||
printf 'Step 2 accepted a remaining IPv6 legacy UFW allow rule\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f -- "$legacy_delete_log"
|
||||
MANAGED_RULES=(80/tcp 443/tcp)
|
||||
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
|
||||
RULE_SEEN=()
|
||||
confirm_legacy_rules <<< 'y'
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 80/tcp '* ]]
|
||||
[[ " ${MANAGED_RULES[*]} " == *' 443/tcp '* ]]
|
||||
SSH_CONNECTION='198.51.100.9 41000 192.0.2.1 20202'
|
||||
MANAGED_RULES=(20202/tcp 443/tcp)
|
||||
RULE_SEEN=()
|
||||
confirm_legacy_rules <<< 'n'
|
||||
add_default_rules
|
||||
[[ " ${MANAGED_RULES[*]} " != *' 22/tcp '* ]]
|
||||
|
||||
# sudo commonly clears SSH_CONNECTION. A first run should still retain the
|
||||
# conventional SSH port if no sshd socket can be discovered, while a later
|
||||
# run must not resurrect 22 after the managed list records its retirement.
|
||||
unset SSH_CONNECTION
|
||||
ss() { :; }
|
||||
ssh_probe_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$ssh_probe_dir"' EXIT
|
||||
mapfile -t current_ssh_ports < <(discover_current_ssh_ports "$ssh_probe_dir/missing-managed-file")
|
||||
[[ " ${current_ssh_ports[*]} " == *' 22 '* ]]
|
||||
touch "$ssh_probe_dir/retired-managed-file"
|
||||
mapfile -t current_ssh_ports < <(discover_current_ssh_ports "$ssh_probe_dir/retired-managed-file")
|
||||
[[ " ${current_ssh_ports[*]} " != *' 22 '* ]]
|
||||
STEP2_TEST
|
||||
|
||||
bash <<'PANEL_TEST'
|
||||
set -Eeuo pipefail
|
||||
source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-panel-warp-6.sh)
|
||||
OPENSSL_FAKE_VALUE=0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
openssl() { printf '%s' "$OPENSSL_FAKE_VALUE"; }
|
||||
XUI_EXISTING_SUB_PATH=''
|
||||
choose_public_access <<< ''
|
||||
[[ "$XUI_PUBLIC_ACCESS" == no && "$XUI_WEB_LISTEN" == 127.0.0.1 ]]
|
||||
choose_subscription_access <<< ''
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == private && "$XUI_SUB_LISTEN" == 127.0.0.1 ]]
|
||||
choose_subscription_access <<< '2'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == disabled ]]
|
||||
choose_subscription_access <<< '3'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == public ]]
|
||||
[[ "$XUI_SUB_LISTEN" == '' ]]
|
||||
[[ "$XUI_SUB_PATH" =~ ^/[0-9a-f]{48}/$ ]]
|
||||
|
||||
subscription_db_probe="$(mktemp)"
|
||||
SQLITE_TEST_SUB_PATH='/operator-custom/path/'
|
||||
sqlite3() { printf '%s' "$SQLITE_TEST_SUB_PATH"; }
|
||||
load_existing_subscription_path "$subscription_db_probe"
|
||||
[[ "$XUI_EXISTING_SUB_PATH" == '/operator-custom/path/' ]]
|
||||
choose_subscription_access <<< '1'
|
||||
[[ "$XUI_SUB_PATH" == "$XUI_EXISTING_SUB_PATH" ]]
|
||||
choose_subscription_access <<< '2'
|
||||
[[ "$XUI_SUB_PATH" == "$XUI_EXISTING_SUB_PATH" ]]
|
||||
|
||||
XUI_EXISTING_SUB_PATH='/0123456789abcdef0123456789abcdef0123456789abcdef/'
|
||||
choose_subscription_access <<< $'3\n'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == public && "$XUI_SUB_PATH" == "$XUI_EXISTING_SUB_PATH" ]]
|
||||
OPENSSL_FAKE_VALUE=abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef
|
||||
choose_subscription_access <<< $'3\ny'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == public && "$XUI_SUB_PATH" == '/abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef/' ]]
|
||||
|
||||
XUI_EXISTING_SUB_PATH='/operator-custom/path/'
|
||||
choose_subscription_access <<< $'3\n1'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == private && "$XUI_SUB_PATH" == "$XUI_EXISTING_SUB_PATH" ]]
|
||||
OPENSSL_FAKE_VALUE=abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef
|
||||
choose_subscription_access <<< $'3\ny'
|
||||
[[ "$XUI_SUBSCRIPTION_ACCESS" == public && "$XUI_SUB_PATH" == '/abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef/' ]]
|
||||
rm -f -- "$subscription_db_probe"
|
||||
XUI_EXISTING_SUB_PATH=''
|
||||
|
||||
choose_public_access <<< '1'
|
||||
choose_subscription_access <<< '1'
|
||||
set_xui_desired_listeners
|
||||
[[ "$XUI_DESIRED_WEB_LISTEN" == 127.0.0.1 ]]
|
||||
[[ "$XUI_DESIRED_SUB_LISTEN" == 127.0.0.1 ]]
|
||||
[[ "$XUI_DESIRED_SUB_ENABLED" == true ]]
|
||||
choose_public_access <<< '2'
|
||||
choose_subscription_access <<< '3'
|
||||
set_xui_desired_listeners
|
||||
[[ "$XUI_DESIRED_WEB_LISTEN" == '' ]]
|
||||
[[ "$XUI_DESIRED_SUB_LISTEN" == '' ]]
|
||||
[[ "$XUI_DESIRED_SUB_ENABLED" == true ]]
|
||||
choose_subscription_access <<< '2'
|
||||
set_xui_desired_listeners
|
||||
[[ "$XUI_DESIRED_SUB_LISTEN" == 127.0.0.1 ]]
|
||||
[[ "$XUI_DESIRED_SUB_ENABLED" == false ]]
|
||||
|
||||
ss() {
|
||||
case "${SS_MODE:-private}" in
|
||||
private) printf 'LISTEN 0 4096 127.0.0.1:%s 0.0.0.0:*\n' "$SS_PORT" ;;
|
||||
delayed)
|
||||
local calls
|
||||
calls="$(cat "$SS_CALLS_FILE")"
|
||||
calls=$((calls + 1))
|
||||
printf '%s\n' "$calls" > "$SS_CALLS_FILE"
|
||||
((calls > 2)) && printf 'LISTEN 0 4096 127.0.0.1:%s 0.0.0.0:*\n' "$SS_PORT"
|
||||
;;
|
||||
wildcard) printf 'LISTEN 0 4096 0.0.0.0:%s 0.0.0.0:*\n' "$SS_PORT" ;;
|
||||
ipv6) printf 'LISTEN 0 4096 [::]:%s [::]:*\n' "$SS_PORT" ;;
|
||||
esac
|
||||
}
|
||||
SS_PORT=2053
|
||||
SS_MODE=delayed
|
||||
SS_CALLS_FILE="$(mktemp)"
|
||||
printf '0\n' > "$SS_CALLS_FILE"
|
||||
sleep() { :; }
|
||||
wait_for_tcp_listener 2053 panel
|
||||
[[ "$(cat "$SS_CALLS_FILE")" -eq 3 ]]
|
||||
verify_private_listener 2053 panel
|
||||
rm -f -- "$SS_CALLS_FILE"
|
||||
SS_MODE=private
|
||||
for SS_MODE in wildcard ipv6; do
|
||||
if (verify_private_listener 2053 panel) >/dev/null 2>&1; then
|
||||
printf 'Private listener check accepted %s bind\n' "$SS_MODE" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
guard_test_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$guard_test_dir"' EXIT
|
||||
cat > "$guard_test_dir/fake-x-ui" <<'FAKE_XUI'
|
||||
#!/usr/bin/env bash
|
||||
printf 'bind:%s\n' "$*" >> "$GUARD_LOG"
|
||||
[[ "${XUI_FAKE_FAIL:-no}" != yes ]]
|
||||
FAKE_XUI
|
||||
cat > "$guard_test_dir/real-systemctl" <<'REAL_SYSTEMCTL'
|
||||
#!/usr/bin/env bash
|
||||
printf 'systemctl:%s\n' "$*" >> "$GUARD_LOG"
|
||||
REAL_SYSTEMCTL
|
||||
cat > "$guard_test_dir/real-rc-service" <<'REAL_RC_SERVICE'
|
||||
#!/usr/bin/env bash
|
||||
printf 'rc-service:%s\n' "$*" >> "$GUARD_LOG"
|
||||
REAL_RC_SERVICE
|
||||
cat > "$guard_test_dir/fake-sqlite3" <<'FAKE_SQLITE3'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "$#" == 1 ]]; then
|
||||
cat >/dev/null
|
||||
printf 'sqlite-write\n' >> "$GUARD_LOG"
|
||||
[[ "${SQLITE_FAKE_FAIL:-no}" != yes ]]
|
||||
else
|
||||
printf 'sqlite-query\n' >> "$GUARD_LOG"
|
||||
printf '127.0.0.1|false\n'
|
||||
fi
|
||||
FAKE_SQLITE3
|
||||
touch "$guard_test_dir/fake.db"
|
||||
chmod 700 "$guard_test_dir/fake-x-ui" "$guard_test_dir/real-systemctl" \
|
||||
"$guard_test_dir/real-rc-service" "$guard_test_dir/fake-sqlite3"
|
||||
export GUARD_LOG="$guard_test_dir/calls.log"
|
||||
create_xui_start_guard "$guard_test_dir/guard" "$guard_test_dir/released" \
|
||||
"$guard_test_dir/fake-x-ui" "$guard_test_dir/fake.db" \
|
||||
"$guard_test_dir/real-systemctl" "$guard_test_dir/real-rc-service" "$guard_test_dir/fake-sqlite3"
|
||||
export PATH="${XUI_GUARD_DIR}:${PATH}"
|
||||
systemctl start x-ui.service
|
||||
[[ "$(sed -n '1p' "$GUARD_LOG")" == 'bind:setting -listenIP 127.0.0.1' ]]
|
||||
[[ "$(sed -n '2p' "$GUARD_LOG")" == 'sqlite-write' ]]
|
||||
[[ "$(tail -n1 "$GUARD_LOG")" == 'systemctl:start x-ui.service' ]]
|
||||
: > "$GUARD_LOG"
|
||||
export XUI_FAKE_FAIL=yes
|
||||
if systemctl start x-ui; then
|
||||
printf 'Guard allowed an x-ui start after bind failure\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
! grep -q '^systemctl:' "$GUARD_LOG"
|
||||
unset XUI_FAKE_FAIL
|
||||
: > "$GUARD_LOG"
|
||||
export SQLITE_FAKE_FAIL=yes
|
||||
if systemctl start x-ui; then
|
||||
printf 'Guard allowed an x-ui start after safe subscription settings failed\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
! grep -q '^systemctl:' "$GUARD_LOG"
|
||||
: > "$GUARD_LOG"
|
||||
unset SQLITE_FAKE_FAIL
|
||||
: > "$GUARD_LOG"
|
||||
touch "$guard_test_dir/released"
|
||||
systemctl start x-ui
|
||||
[[ "$(wc -l < "$GUARD_LOG")" -eq 1 ]]
|
||||
[[ "$(sed -n '1p' "$GUARD_LOG")" == 'systemctl:start x-ui' ]]
|
||||
PANEL_TEST
|
||||
|
||||
bash <<'STEP3_TEST'
|
||||
set -Eeuo pipefail
|
||||
source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-user-3.sh)
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$tmp_dir"' EXIT
|
||||
MANAGE_UFW=yes
|
||||
SSH_PORT=20202
|
||||
UFW_TEST_STATUS='Status: active'
|
||||
ufw() {
|
||||
if [[ "${1:-}" == status ]]; then
|
||||
printf '%s\n' "$UFW_TEST_STATUS"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$*" >> "$tmp_dir/ufw-calls"
|
||||
return 0
|
||||
}
|
||||
require_command() { :; }
|
||||
chown() { :; }
|
||||
printf '22/tcp\n20202/tcp\n443/tcp\n' > "$tmp_dir/managed"
|
||||
retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'n'
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
retire_temporary_ssh_port_22 "$tmp_dir/managed" </dev/null
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'y'
|
||||
! grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
grep -Fqx '20202/tcp' "$tmp_dir/managed"
|
||||
grep -Fq 'from 0.0.0.0/0 to any port 22 proto tcp' "$tmp_dir/ufw-calls"
|
||||
grep -Fq 'from ::/0 to any port 22 proto tcp' "$tmp_dir/ufw-calls"
|
||||
printf '22/tcp\n' > "$tmp_dir/managed"
|
||||
MANAGE_UFW=no
|
||||
retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'y'
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
MANAGE_UFW=yes
|
||||
SSH_PORT=22
|
||||
retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'y'
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
UFW_TEST_STATUS=$'Status: active\nTo Action From\n22/tcp ALLOW IN Anywhere'
|
||||
SSH_PORT=20202
|
||||
if (retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'y') >/dev/null 2>&1; then
|
||||
printf 'Step 3 reported success while UFW still allows port 22\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
UFW_TEST_STATUS=$'Status: active\nTo Action From\n22/tcp (v6) ALLOW IN Anywhere (v6)'
|
||||
if (retire_temporary_ssh_port_22 "$tmp_dir/managed" <<< 'y') >/dev/null 2>&1; then
|
||||
printf 'Step 3 reported success while UFW still allows IPv6 port 22\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Fqx '22/tcp' "$tmp_dir/managed"
|
||||
STEP3_TEST
|
||||
|
||||
if grep -q 'allow_ufw_rule "63636/tcp"' ochenstarik-server-user-3.sh; then
|
||||
printf 'Step 3 must not open an unsolicited 63636/tcp rule\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'allow_ufw_port 80 ' ochenstarik-server-panel-warp-6.sh; then
|
||||
printf 'Step 6 must not open an unsolicited 80/tcp rule\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'XUI_PUBLIC_ACCESS=no' ochenstarik-server-panel-warp-6.sh
|
||||
grep -q 'Приватный режим' ochenstarik-server-panel-warp-6.sh
|
||||
grep -q 'WARP_ANSWER=no' ochenstarik-server-panel-warp-6.sh
|
||||
|
||||
printf '[+] Firewall and panel security behavior passed\n'
|
||||
29
tests/test-monitor-manager-control-layer.sh
Normal file
29
tests/test-monitor-manager-control-layer.sh
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
script="ochenstarik-server-monitor-manager.sh"
|
||||
|
||||
[[ -s "$script" ]]
|
||||
grep -Fq 'readonly SMM_RELEASE_VERSION=' "$script"
|
||||
grep -Fq 'server-monitor-manager-${runtime}.tar.gz' "$script"
|
||||
grep -Fq 'sha256sum "$archive"' "$script"
|
||||
grep -Fq 'SMMCTL1-' "$script"
|
||||
grep -Fq 'SMMDEV1-' "$script"
|
||||
grep -Fq 'control-code)' "$script"
|
||||
grep -Fq 'install-control-hub)' "$script"
|
||||
grep -Fq 'install-control-agent)' "$script"
|
||||
grep -Fq 'control-device-code)' "$script"
|
||||
grep -Fq 'ochenstarik-smm-policy-apply' "$script"
|
||||
grep -Fq 'NoNewPrivileges=true' "$script"
|
||||
grep -Fq 'ProtectSystem=strict' "$script"
|
||||
grep -Fq 'SMM_EnrollToken=${token}' "$script"
|
||||
grep -Fq 'systemctl enable --now ochenstarik-smm-control.service' "$script"
|
||||
grep -Fq 'systemctl enable --now ochenstarik-smm-agent.service' "$script"
|
||||
|
||||
if grep -Eq 'CONTROL_JOIN_CODE=.*(ca\.key|control-ca\.pfx)' "$script"; then
|
||||
echo 'Control join code must never contain the CA private key.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Server Monitor Manager control-layer installer checks passed.\n'
|
||||
99
tests/test-monitor-manager-enrollment.sh
Normal file
99
tests/test-monitor-manager-enrollment.sh
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
script="ochenstarik-server-monitor-manager.sh"
|
||||
temp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$temp_dir"' EXIT
|
||||
|
||||
[[ -s "$script" ]]
|
||||
grep -Fq 'code="SMM2-' "$script"
|
||||
grep -Fq 'request_code="SMMREQ1-' "$script"
|
||||
grep -Fq 'SMMACK1-' "$script"
|
||||
grep -Fq 'expires=$(( $(date +%s) + 600 ))' "$script"
|
||||
grep -Fq 'mv -- "$token_file" "$consuming_file"' "$script"
|
||||
grep -Fq 'private_key="$(wg genkey)"' "$script"
|
||||
|
||||
if grep -Fq 'PRIVATE_KEY=%s' "$script"; then
|
||||
echo 'Enrollment payload must not contain a private WireGuard key.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
helper="$temp_dir/ochenstarik-smm-hub"
|
||||
awk '
|
||||
capture && /^EOF$/ { exit }
|
||||
/cat > .*HUB_HELPER.*<</ { capture=1; next }
|
||||
capture { print }
|
||||
' "$script" > "$helper"
|
||||
chmod 700 "$helper"
|
||||
|
||||
state="$temp_dir/state"
|
||||
mock_bin="$temp_dir/bin"
|
||||
mkdir -p "$state/nodes" "$state/tokens" "$mock_bin"
|
||||
sed -i "s|^readonly STATE_DIR=.*|readonly STATE_DIR=\"$state\"|" "$helper"
|
||||
sed -i "s|^readonly WG_CONFIG=.*|readonly WG_CONFIG=\"$temp_dir/smm0.conf\"|" "$helper"
|
||||
sed -i 's/^require_root() .*/require_root() { :; }/' "$helper"
|
||||
sed -i 's/ -o root -g root//g' "$helper"
|
||||
|
||||
public_key='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||
cat > "$mock_bin/wg" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
if [[ "\${1:-}" == pubkey ]]; then
|
||||
printf '%s\n' '$public_key'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
cat > "$mock_bin/ip" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 1
|
||||
EOF
|
||||
chmod 700 "$mock_bin/wg" "$mock_bin/ip"
|
||||
|
||||
cat > "$state/hub.conf" <<'EOF'
|
||||
HUB_ENDPOINT=hub.example.test
|
||||
WG_PORT=51820
|
||||
HUB_ADDRESS=10.77.0.1
|
||||
MESH_CIDR=10.77.0.0/24
|
||||
EOF
|
||||
printf 'test-hub-private-key\n' > "$state/hub.key"
|
||||
: > "$state/links"
|
||||
|
||||
output="$(PATH="$mock_bin:$PATH" "$helper" node-code test-node)"
|
||||
join_code="$(printf '%s\n' "$output" | grep '^SMM2-')"
|
||||
[[ -n "$join_code" ]]
|
||||
|
||||
decode_base64url() {
|
||||
local data="$1"
|
||||
data="${data//-/+}"
|
||||
data="${data//_/\/}"
|
||||
case $(( ${#data} % 4 )) in
|
||||
2) data+='==' ;;
|
||||
3) data+='=' ;;
|
||||
esac
|
||||
printf '%s' "$data" | base64 -d
|
||||
}
|
||||
|
||||
payload="$(decode_base64url "${join_code#SMM2-}")"
|
||||
token="$(printf '%s\n' "$payload" | awk -F= '$1 == "TOKEN" { print $2 }')"
|
||||
name="$(printf '%s\n' "$payload" | awk -F= '$1 == "NAME" { print $2 }')"
|
||||
address="$(printf '%s\n' "$payload" | awk -F= '$1 == "ADDRESS" { print $2 }')"
|
||||
[[ "$name" == test-node ]]
|
||||
[[ "$address" == 10.77.0.2/32 ]]
|
||||
[[ "$token" =~ ^[a-f0-9]{64}$ ]]
|
||||
[[ "$payload" != *PRIVATE_KEY* ]]
|
||||
|
||||
request_payload="$(printf 'VERSION=1\nTOKEN=%s\nNAME=%s\nADDRESS=%s\nPUBLIC_KEY=%s\n' \
|
||||
"$token" "$name" "$address" "$public_key")"
|
||||
request_code="SMMREQ1-$(printf '%s' "$request_payload" | base64 -w0 | tr '+/' '-_' | tr -d '=')"
|
||||
enroll_output="$(PATH="$mock_bin:$PATH" SMM_ENROLL_REQUEST="$request_code" "$helper" node-enroll)"
|
||||
grep -Eq '^SMMACK1-[a-f0-9]{64}$' <<< "$enroll_output"
|
||||
grep -Fqx 'NAME=test-node' "$state/nodes/test-node.node"
|
||||
grep -Fqx "PUBLIC_KEY=$public_key" "$state/nodes/test-node.node"
|
||||
|
||||
if PATH="$mock_bin:$PATH" SMM_ENROLL_REQUEST="$request_code" "$helper" node-enroll >/dev/null 2>&1; then
|
||||
echo 'Enrollment token was accepted more than once.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Server Monitor Manager enrollment checks passed.\n'
|
||||
22
tests/test-monitor-manager-lifecycle.sh
Normal file
22
tests/test-monitor-manager-lifecycle.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
script="ochenstarik-server-monitor-manager.sh"
|
||||
|
||||
[[ -s "$script" ]]
|
||||
grep -Fq 'sudo' "$script"
|
||||
grep -Fq 'iputils-ping' "$script"
|
||||
grep -Fq 'backup_state()' "$script"
|
||||
grep -Fq 'rollback_state()' "$script"
|
||||
grep -Fq 'update_installed()' "$script"
|
||||
grep -Fq 'uninstall_monitor()' "$script"
|
||||
grep -Fq 'uninstall_node()' "$script"
|
||||
grep -Fq 'uninstall_hub()' "$script"
|
||||
grep -Fq '/var/backups/${APP_NAME}' "$script"
|
||||
grep -Fq 'systemctl disable --now ochenstarik-smm-firewall.timer' "$script"
|
||||
grep -Fq 'nft delete table inet ochenstarik_smm' "$script"
|
||||
grep -Fq 'sysctl --system' "$script"
|
||||
grep -Fq 'uninstall) die "Укажите роль:' "$script"
|
||||
|
||||
printf 'Server Monitor Manager lifecycle checks passed.\n'
|
||||
95
tests/test-monitor-manager-links.sh
Normal file
95
tests/test-monitor-manager-links.sh
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
script="ochenstarik-server-monitor-manager.sh"
|
||||
temp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$temp_dir"' EXIT
|
||||
|
||||
helper="$temp_dir/ochenstarik-smm-hub"
|
||||
awk '
|
||||
capture && /^EOF$/ { exit }
|
||||
/cat > .*HUB_HELPER.*<</ { capture=1; next }
|
||||
capture { print }
|
||||
' "$script" > "$helper"
|
||||
chmod 700 "$helper"
|
||||
|
||||
state="$temp_dir/state"
|
||||
mock_bin="$temp_dir/bin"
|
||||
nft_capture="$temp_dir/nft.conf"
|
||||
mkdir -p "$state/nodes" "$state/tokens" "$mock_bin"
|
||||
sed -i "s|^readonly STATE_DIR=.*|readonly STATE_DIR=\"$state\"|" "$helper"
|
||||
sed -i "s|^readonly WG_CONFIG=.*|readonly WG_CONFIG=\"$temp_dir/smm0.conf\"|" "$helper"
|
||||
sed -i 's/^require_root() .*/require_root() { :; }/' "$helper"
|
||||
sed -i 's/ -o root -g root//g' "$helper"
|
||||
|
||||
cat > "$mock_bin/nft" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
previous=''
|
||||
for argument in "$@"; do
|
||||
if [[ "$previous" == -f ]]; then
|
||||
cp -- "$argument" "$NFT_CAPTURE"
|
||||
fi
|
||||
previous="$argument"
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
chmod 700 "$mock_bin/nft"
|
||||
|
||||
cat > "$state/hub.conf" <<'EOF'
|
||||
HUB_ENDPOINT=hub.example.test
|
||||
WG_PORT=51820
|
||||
HUB_ADDRESS=10.77.0.1
|
||||
MESH_CIDR=10.77.0.0/24
|
||||
EOF
|
||||
printf 'test-hub-private-key\n' > "$state/hub.key"
|
||||
cat > "$state/nodes/ai-agent.node" <<'EOF'
|
||||
NAME=ai-agent
|
||||
ADDRESS=10.77.0.2
|
||||
PUBLIC_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
EOF
|
||||
cat > "$state/nodes/home.node" <<'EOF'
|
||||
NAME=home
|
||||
ADDRESS=10.77.0.3
|
||||
PUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
|
||||
EOF
|
||||
printf 'legacy-policy home\n' > "$state/links"
|
||||
|
||||
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
|
||||
"$helper" link-connect ai-agent home tcp 2222 120
|
||||
|
||||
read -r source target cidr protocol port expires version < "$state/links"
|
||||
[[ "$source" == ai-agent ]]
|
||||
[[ "$target" == home ]]
|
||||
[[ "$cidr" == 10.77.0.3/32 ]]
|
||||
[[ "$protocol" == tcp ]]
|
||||
[[ "$port" == 2222 ]]
|
||||
(( expires > $(date +%s) ))
|
||||
[[ "$version" =~ ^[0-9]+$ ]]
|
||||
grep -Fq 'ip saddr 10.77.0.2 ip daddr 10.77.0.3/32 tcp dport 2222 accept' "$nft_capture"
|
||||
grep -Fq '"state":"Connecting"' "$state/audit.jsonl"
|
||||
grep -Fq '"state":"Active"' "$state/audit.jsonl"
|
||||
|
||||
if PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
|
||||
"$helper" link-connect ai-agent home tcp 70000 120 >/dev/null 2>&1; then
|
||||
echo 'Invalid port was accepted.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'ai-agent home 10.77.0.3/32 udp 53 1 99\n' >> "$state/links"
|
||||
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" "$helper" firewall-restore
|
||||
[[ "$(wc -l < "$state/links")" -eq 1 ]]
|
||||
|
||||
PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \
|
||||
"$helper" link-disconnect ai-agent home tcp 2222
|
||||
[[ ! -s "$state/links" ]]
|
||||
grep -Fq '"state":"Expired"' "$state/audit.jsonl"
|
||||
grep -Fq '"state":"Disconnecting"' "$state/audit.jsonl"
|
||||
grep -Fq '"state":"Disabled"' "$state/audit.jsonl"
|
||||
if grep -Fq 'dport 2222 accept' "$nft_capture"; then
|
||||
echo 'Disconnected policy remained in nftables.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -Fq 'OnUnitActiveSec=1min' "$script"
|
||||
printf 'Server Monitor Manager Link policy checks passed.\n'
|
||||
15
tests/test-monitor-manager-metrics.sh
Normal file
15
tests/test-monitor-manager-metrics.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
script="ochenstarik-server-monitor-manager.sh"
|
||||
|
||||
for key in \
|
||||
SWAP_TOTAL_KB SWAP_FREE_KB \
|
||||
DISK_INODES_TOTAL DISK_INODES_FREE \
|
||||
NETWORK_RX_BYTES NETWORK_TX_BYTES \
|
||||
SYSTEMD_SSH SYSTEMD_WIREGUARD; do
|
||||
grep -Fq "$key" "$script"
|
||||
done
|
||||
|
||||
printf 'Server Monitor Manager extended metrics checks passed.\n'
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$tmp"' EXIT
|
||||
mkdir -p "$tmp/bin"
|
||||
|
||||
# Extract and run the exact hook template without writing to PAM or contacting
|
||||
# Telegram. The stubs record invocation and deliberately make curl slow.
|
||||
awk '/^cat > "\$NOTIFY_SCRIPT" <<.HOOK/ { copying=1; next }
|
||||
copying && /^HOOK$/ { exit }
|
||||
copying { print }' ochenstarik-server-tg-4.sh > "$tmp/notify-hook"
|
||||
[[ -s "$tmp/notify-hook" ]]
|
||||
# Relocate only the extracted fixture; production PAM paths are fixed and do
|
||||
# not trust session-provided test overrides.
|
||||
sed -i "s|/etc/ochenstarik-server/telegram.conf|$tmp/config|g; s|/var/log/ochenstarik-ssh-login-telegram.log|$tmp/notify.log|g; s|/usr/bin/setsid|$tmp/bin/setsid|g" "$tmp/notify-hook"
|
||||
bash -n "$tmp/notify-hook"
|
||||
chmod 700 "$tmp/notify-hook"
|
||||
cat > "$tmp/config" <<'CONFIG'
|
||||
TG_BOT_TOKEN=123456:secret-token-marker
|
||||
TG_CHAT_ID=12345
|
||||
SERVER_IP=192.0.2.1
|
||||
CONFIG
|
||||
chmod 600 "$tmp/config"
|
||||
|
||||
cat > "$tmp/bin/setsid" <<'SETSID_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf 'called\n' >> "$SETSID_LOG"
|
||||
exec /usr/bin/setsid "$@"
|
||||
SETSID_STUB
|
||||
cat > "$tmp/bin/curl" <<'CURL_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "$*" >> "$CURL_ARGS_LOG"
|
||||
cat > "$CURL_CONFIG_LOG"
|
||||
touch "$CURL_STARTED"
|
||||
sleep 1
|
||||
touch "$CURL_DONE"
|
||||
printf '200\n' >&2
|
||||
exit 0
|
||||
CURL_STUB
|
||||
chmod +x "$tmp/bin/setsid" "$tmp/bin/curl"
|
||||
|
||||
export PATH="$tmp/bin:$PATH"
|
||||
export SETSID_LOG="$tmp/setsid.log" CURL_ARGS_LOG="$tmp/curl-args"
|
||||
export CURL_CONFIG_LOG="$tmp/curl-config" CURL_STARTED="$tmp/curl-started"
|
||||
export CURL_DONE="$tmp/curl-done"
|
||||
export PAM_USER=test-user PAM_RHOST=192.0.2.7 PAM_SERVICE=sshd PAM_TTY=pts/1
|
||||
|
||||
PAM_TYPE=close_session "$tmp/notify-hook"
|
||||
[[ ! -e "$SETSID_LOG" && ! -e "$CURL_STARTED" ]]
|
||||
|
||||
started_ns="$(date +%s%N)"
|
||||
PAM_TYPE=open_session "$tmp/notify-hook"
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_ns) / 1000000 ))
|
||||
((elapsed_ms < 500)) || { printf 'PAM hook waited %s ms for background work.\n' "$elapsed_ms" >&2; exit 1; }
|
||||
|
||||
for _ in {1..60}; do
|
||||
[[ -e "$CURL_STARTED" ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
[[ -e "$CURL_STARTED" ]]
|
||||
[[ "$(wc -l < "$SETSID_LOG")" == 1 ]]
|
||||
! grep -Fq 'secret-token-marker' "$CURL_ARGS_LOG"
|
||||
grep -Fq 'url = "https://api.telegram.org/bot123456:secret-token-marker/sendMessage"' "$CURL_CONFIG_LOG"
|
||||
|
||||
# Verify the generated backup runner's opt-in encryption and strict offsite
|
||||
# transport contract without invoking it against the host filesystem.
|
||||
awk '/^ cat > "\$BACKUP_RUNNER" <<.BACKUP_RUNNER_EOF/ { copying=1; next }
|
||||
copying && /^BACKUP_RUNNER_EOF$/ { exit }
|
||||
copying { print }' ochenstarik-server-backup-7.sh > "$tmp/backup-runner"
|
||||
bash -n "$tmp/backup-runner"
|
||||
grep -Fq 'BACKUP_ENCRYPTION=none' "$tmp/backup-runner"
|
||||
grep -Fq 'age --encrypt --recipient "$AGE_RECIPIENT" --output "$temporary"' "$tmp/backup-runner"
|
||||
grep -Fq 'StrictHostKeyChecking=yes' "$tmp/backup-runner"
|
||||
grep -Fq 'BatchMode=yes' "$tmp/backup-runner"
|
||||
grep -Fq 'UserKnownHostsFile=$OFFSITE_KNOWN_HOSTS' "$tmp/backup-runner"
|
||||
grep -Fq 'Выгрузка на заданный SSH-хост не удалась' "$tmp/backup-runner"
|
||||
if grep -Fq 'ssh-keyscan' ochenstarik-server-backup-7.sh; then
|
||||
printf 'Backup setup must not trust an unverified ssh-keyscan result.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
awk '/^upload_offsite\(\)/ { copying=1 }
|
||||
copying { print }
|
||||
copying && /^}/ { exit }' "$tmp/backup-runner" > "$tmp/upload-function"
|
||||
source "$tmp/upload-function"
|
||||
log() { :; }
|
||||
die() { printf '%s\n' "$*" >&2; exit 1; }
|
||||
cat > "$tmp/bin/scp" <<'SCP_STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
printf '%s\n' "$*" >> "$SCP_ARGS_LOG"
|
||||
exit "${SCP_EXIT_CODE:-0}"
|
||||
SCP_STUB
|
||||
chmod +x "$tmp/bin/scp"
|
||||
export SCP_ARGS_LOG="$tmp/scp-args"
|
||||
OFFSITE_TARGET='backup@example.test'
|
||||
OFFSITE_PATH='/srv/backups'
|
||||
OFFSITE_KNOWN_HOSTS="$tmp/known_hosts"
|
||||
touch "$OFFSITE_KNOWN_HOSTS" "$tmp/archive.tar.zst.age" "$tmp/archive.sha256"
|
||||
SCP_EXIT_CODE=0 upload_offsite "$tmp/archive.tar.zst.age" "$tmp/archive.sha256"
|
||||
grep -Fq -- '-o BatchMode=yes' "$SCP_ARGS_LOG"
|
||||
grep -Fq -- '-o StrictHostKeyChecking=yes' "$SCP_ARGS_LOG"
|
||||
grep -Fq -- "-o UserKnownHostsFile=$OFFSITE_KNOWN_HOSTS" "$SCP_ARGS_LOG"
|
||||
if (SCP_EXIT_CODE=42 upload_offsite "$tmp/archive.tar.zst.age" "$tmp/archive.sha256") \
|
||||
>/dev/null 2>&1; then
|
||||
printf 'Offsite upload failure was accepted as success.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
for _ in {1..60}; do
|
||||
[[ -e "$CURL_DONE" ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
[[ -e "$CURL_DONE" ]]
|
||||
|
||||
printf 'Notification and backup security tests passed.\n'
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-2.sh)
|
||||
test_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$test_dir"' EXIT
|
||||
|
||||
systemctl() {
|
||||
printf '%s\n' "$*" >> "$test_dir/calls"
|
||||
case "$*" in
|
||||
'is-enabled ufw.service') cat "$test_dir/state"; return 1 ;;
|
||||
'enable ufw.service')
|
||||
[[ "$scenario" != enable-failed ]] || return 1
|
||||
[[ "$scenario" == verify-failed ]] || printf enabled > "$test_dir/state"
|
||||
;;
|
||||
'is-enabled --quiet ufw.service') [[ "$(cat "$test_dir/state")" == enabled ]] ;;
|
||||
*) return 99 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for state in masked masked-runtime; do
|
||||
printf '%s' "$state" > "$test_dir/state"
|
||||
: > "$test_dir/calls"
|
||||
scenario=masked
|
||||
if (ensure_ufw_boot_service) > "$test_dir/output" 2>&1; then exit 1; fi
|
||||
! grep -q '^enable ' "$test_dir/calls"
|
||||
[[ "$(cat "$test_dir/state")" == "$state" ]]
|
||||
done
|
||||
|
||||
for scenario in disabled enabled enable-failed verify-failed; do
|
||||
printf '%s' "$scenario" > "$test_dir/state"
|
||||
: > "$test_dir/calls"
|
||||
if [[ "$scenario" == *-failed ]]; then
|
||||
if (ensure_ufw_boot_service) > "$test_dir/output" 2>&1; then exit 1; fi
|
||||
else
|
||||
ensure_ufw_boot_service
|
||||
[[ "$(cat "$test_dir/state")" == enabled ]]
|
||||
grep -qx 'is-enabled --quiet ufw.service' "$test_dir/calls"
|
||||
fi
|
||||
done
|
||||
printf 'Step 2 UFW boot readiness tests passed.\n'
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
|
||||
source <(awk '/^\[\[ "\$EUID"/{exit} {print}' ochenstarik-server-user-3.sh)
|
||||
test_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$test_dir"' EXIT
|
||||
|
||||
# Relocate just the privileged filesystem boundary; use the real install
|
||||
# binary to check first creation, idempotency and repair of unsafe modes.
|
||||
install() {
|
||||
[[ "$*" == '-d -o root -g root -m 755 /run/sshd' ]]
|
||||
/usr/bin/install -d -m 755 "$test_dir/sshd"
|
||||
}
|
||||
prepare_sshd_runtime_directory
|
||||
[[ -d "$test_dir/sshd" && "$(stat -c %a "$test_dir/sshd")" == 755 ]]
|
||||
chmod 777 "$test_dir/sshd"
|
||||
prepare_sshd_runtime_directory
|
||||
[[ "$(stat -c %a "$test_dir/sshd")" == 755 ]]
|
||||
|
||||
# The runtime prerequisite must run before standalone configuration validation.
|
||||
prepare_line="$(grep -n '^prepare_sshd_runtime_directory$' ochenstarik-server-user-3.sh | cut -d: -f1)"
|
||||
validate_line="$(grep -n '^sshd -t ' ochenstarik-server-user-3.sh | cut -d: -f1)"
|
||||
((prepare_line < validate_line))
|
||||
printf 'Step 3 socket-activation runtime directory tests passed.\n'
|
||||
Loading…
Reference in a new issue