# PasarGuard node — آینه‌ی آفلاین

نصب کامل سرویس node پروژه‌ی PasarGuard بدون هیچ دسترسی به اینترنت بین‌الملل.
همه‌ی فایل‌ها، باینری‌ها و ایمیج داکر از همین دامنه سرو می‌شوند.

## نصب

```bash
curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 20 \
  https://git.nimbaha.org/pgnode/pg-node-offline.sh -o /tmp/pg-node.sh \
  && sudo bash /tmp/pg-node.sh install
```

`pg-node-offline.sh` نسخه‌ی **تک‌فایل** است: چهار کتابخانه در خودش inline شده‌اند،
پس کل نصب فقط یک درخواست به میرور می‌زند. نسخه‌ی اصلی بالا‌دست برای گرفتن
کتابخانه‌ها چهار درخواست جدا می‌زند و اگر یکی شکست بخورد همه را دور می‌ریزد؛
روی سروری با DNS ناپایدار این یک پرتاب سکه است.

`pg-node.sh` هم برای سازگاری سر جایش هست، ولی تک‌فایل را ترجیح بده.

### اگر DNS سرور ناپایدار است

با `--resolve` می‌شود DNS را کامل دور زد — گواهی TLS همچنان برای نام دامنه
اعتبارسنجی می‌شود، پس امنیت کم نمی‌شود:

```bash
curl -fsSL --resolve git.nimbaha.org:443:85.9.112.198 \
  https://git.nimbaha.org/pgnode/pg-node-offline.sh -o /tmp/pg-node.sh \
  && sudo bash /tmp/pg-node.sh install
```

یا یک بار برای همیشه:

```bash
echo "85.9.112.198 git.nimbaha.org" >> /etc/hosts
```

## پشتیبانی

| مورد | مقدار |
|---|---|
| معماری | `x86_64` و `aarch64` |
| توزیع | هر توزیع گلیبک‌محور — داکر از باینری استاتیک نصب می‌شود |
| نسخه‌ی ایمیج | `v0.5.7` |
| Docker | 29.9.3 (static) |
| Compose | v5.7.0 (plugin) |
| yq | v4.54.2 |
| Xray-core | v26.10.31 |
| jq | 1.8.2 (static) |

سرویس با `systemd` هم نصب می‌شود (`install-service`) و باینری `node-serviced`
برای هر چهار معماری آینه شده است.

## میرور موقت بسته‌های سیستمی

نصب بسته‌های توزیع تنها مرحله‌ای است که به مخزن بسته نیاز دارد. روی سروری که
دسترسی بین‌المللی ندارد این مخزن مرده است، بنابراین اسکریپت:

1. اول مسیر عادی را امتحان می‌کند.
2. فقط اگر شکست خورد، مخازن را **موقتاً** به یک میرور ایرانی سوییچ می‌کند
   (اولین میرور پاسخ‌ده از فهرست، پس اگر یکی پایین بود بعدی امتحان می‌شود).
3. پیش از هر تغییری نسخه‌ی اصلی را در `/var/backups/pasarguard-sources` بکاپ
   می‌گیرد و `trap` روی `EXIT INT TERM` می‌گذارد.
4. با هر شکل خروجی — موفق، خطا، یا `Ctrl-C` — تنظیمات اصلی بایت‌به‌بایت
   برمی‌گردد و پیام «Package repositories restored» چاپ می‌شود.

میرورهای آزمایش‌شده:

| توزیع | میرور |
|---|---|
| Ubuntu | `mirror.arvancloud.ir/ubuntu` → `repo.iut.ac.ir/ubuntu` → `mirror.iranserver.com/ubuntu` |
| Debian | `mirror.arvancloud.ir/debian` + `debian-security` → `repo.iut.ac.ir/debian` |
| AlmaLinux / Rocky / EL | `mirror.iranserver.com/almalinux` → `mirror.arvancloud.ir/almalinux` |

سنجش واقعی روی `focal`: ۴۶٫۳ مگابایت ایندکس در ۱۱ ثانیه (۴٫۲ MB/s).

میرور **هرگز** پیش‌دستانه فعال نمی‌شود و روی نصب سالم هیچ اثری ندارد.

## چه چیزی تغییر کرده

1. **`get.docker.com`** — به جای اسکریپت رسمی (که مخزن apt/dnf را به
   `download.docker.com` وصل می‌کند)، تاربال استاتیک داکر در `/usr/local/bin`
   نصب می‌شود، همراه با unit فایل `systemd`.
2. **پلاگین `docker compose`** — از مخزن توزیع نصب نمی‌شود؛ باینری در
   `/usr/local/lib/docker/cli-plugins/` گذاشته می‌شود.
3. **`docker compose pull`** — ایمیج از تاربال محلی `docker load` می‌شود و
   `pull_policy: never` ست می‌شود تا هیچ‌وقت به رجیستری مراجعه نکند.
4. **`api.github.com`** — پاسخ‌ها به صورت JSON ثابت سرو می‌شوند.
5. **بارگذاری کتابخانه‌ها** — در باندل تک‌فایل کاملاً inline شده، چون بوت‌استرپ
   چهار-درخواستی بالا‌دست روی DNS ناپایدار کل نصب را از بین می‌برد.
6. **بسته‌های سیستمی** — `jq` استاتیک، و میرور موقت ایرانی به عنوان آخرین راه.

## محدودیت‌ها

- **یک اسنپ‌شات ایمیج.** آینه فقط ایمیج `v0.5.7` را دارد. اگر `--version`
  دیگری بخواهید، اسکریپت هشدار صریح می‌دهد؛ بی‌صدا نسخه‌ی اشتباه را جا نمی‌زند.
- برای انتشار نسخه‌ی جدید باید `sync-pgnode.sh` دوباره اجرا شود.
- `raw.githubusercontent.com` از سرور آینه قابل دریافت نیست؛ فایل‌های raw از
  پوشه‌ی seed بارگذاری می‌شوند.

## ساختار

```
pgnode/
├── pg-node-offline.sh          ← تک‌فایل، این را استفاده کن
├── pg-node.sh                  نقطه‌ی ورود اصلی (پچ‌شده)
├── github/…                    محتوای github.com با همان مسیر
├── raw/…                       محتوای raw.githubusercontent.com
├── api/…                       پاسخ‌های api.github.com به صورت JSON ثابت
├── bin/docker-static/          داکر استاتیک، دو معماری
├── bin/compose/                پلاگین compose
├── bin/jq/                     jq استاتیک
├── images/                     node image به صورت docker-archive
└── versions.json               نسخه‌های آینه‌شده
```

مسیرها عیناً همان مسیرهای بالادست هستند و فقط میزبان عوض شده؛ به همین دلیل
پچ تا این حد کوچک است.

## نگهداری

```bash
/usr/local/bin/sync-pgnode.sh            # 1) همگام‌سازی از بالا‌دست
python3 /usr/local/bin/patch-pgnode.py   # 2) اعمال پچ‌ها
python3 /usr/local/bin/build-bundle.py   # 3) ساخت تک‌فایل
/usr/local/bin/verify-pgnode.sh          # 4) اعتبارسنجی کامل
```

`patch-pgnode.py` هر جایگزینی را assert می‌کند: اگر بالا‌دست تغییر کند و الگویی
پیدا نشود، با خطا متوقف می‌شود — نه این‌که نسخه‌ی نیمه‌پچ‌شده منتشر کند.

## همگام‌سازی خودکار (هفتگی)

```
/etc/cron.d/marzban-sync   0 4  * * 1   (دوشنبه ۰۴:۰۰)
/etc/cron.d/pgnode-sync   20 4  * * 1   (دوشنبه ۰۴:۲۰)
/etc/cron.d/sync-3xui      0 3  * * *   (هر روز ۰۳:۰۰)
```

`sync-pgnode.sh` حالا این کارها را می‌کند:

1. **اسکریپت‌های بالادست را از رله می‌کشد** (`raw.githubusercontent.com` روی
   `85.9.112.219`). اگر رله در دسترس نباشد، نسخه‌ی قبلی دست‌نخورده می‌ماند.
2. باینری‌های داکر، compose، yq، Xray و دیتاست‌های geo را تازه می‌کند.
3. `_node_releases.json` را به‌روز می‌کند — همان فایلی که منوی نسخه‌ها از آن
   خوانده می‌شود و قبلاً یک‌بار ساخته شده بود و دیگر تازه نمی‌شد.
4. **پچ را دوباره اعمال می‌کند** و اگر پچر روی نسخه‌ی جدید بالادست نتواند
   لنگر لازم را پیدا کند، نسخه‌ی سالم قبلی را برمی‌گرداند و با خطا خارج می‌شود
   (بکاپ در `/var/lib/pgnode-lastgood`).
5. ایمیج را فقط وقتی digest عوض شده باشد می‌کشد (`images/.digest-amd64`).
6. در پایان `verify-pgnode.sh` را اجرا می‌کند؛ خروجی غیرصفر در لاگ می‌ماند.

لاگ: `/var/log/sync-pgnode.log`

### چرا دو فایل جدا برای مخازن ایرانی

`patch-pgnode.py` و `patch-marzban.py` هر دو ماژول `_pg_ir_*` را inline
می‌کنند، ولی پچر PasarGuard علاوه بر آن تابع `try_install_package()` را لازم
دارد (چون `install_package()` را جایگزین می‌کند و نسخه‌ی اصلی را به
`_try_install_package_impl()` تغییر نام می‌دهد). به همین دلیل دو کپی وجود دارد:

```
/usr/local/bin/ir-mirror-pgnode.inc.sh    ماژول + try_install_package()
/usr/local/bin/ir-mirror-marzban.inc.sh   فقط ماژول
```

این‌ها را با هم یکی نکن. اگر یکی روی دیگری نوشته شود، نصاب PasarGuard تابعی را
صدا می‌زند که وجود ندارد و نصب با `command not found` می‌میرد — دقیقاً همان
اتفاقی که یک‌بار رخ داد و با ۴۰۳ روی شش فایل خودش را نشان داد.
