# Організація локального дзеркала залежностей Cargo з Kellnr Давно задумуюсь про організацію локального дзеркала залежностей crates.io, щоб не ходити за ними в Інтернет. При чому, я не маю достатньо простору для хостингу повної копії репозиторію, утім готовий ділитися тими крейтами, якими користуюся сам. Трішки прозондувавши тему, віднайшов проєкт Kellnr: => https://kellnr.io Це рішення позиціонується як Self Hosted Solution для хостингу крейтів, та головне - має фічу вбудованого кешуючого проксі, що власне під мої задачі підходить. Дане рішення постачається з Веб-інтерфейсом для адміністрування: зокрема, керування правами доступу. => rust-crates-mirroring-with-kellnr/index-page.png Головна сторінка каталогу (скріншот) Сторінка пошуку містить відповідну перемичку фільтрації: => rust-crates-mirroring-with-kellnr/proxy-filter.png Фільтрація кешованих пакунків в Kellnr (скріншот) Отже, має бути зручно, особливо для візуалізації результатів CLI-експериментів. Нижче занотую процес встановлення і налаштування публічної ноди (бо варіант з Docker мені не підходить як і автоматичне встановлення скриптом `.sh` - якщо пускатиму в контейнері, то робитиму це руками в LXC) ## Встановлення ``` bash git clone https://github.com/kellnr/kellnr.git cd kellnr ``` Якщо планується компіляція кореневого `Cargo.toml` то потрібно спочатку зібрати компонент `ui`, інакше буде помилка на етапі `crates/embedded-resources`: ``` bash cd ui npm install npm run build ``` На цьому етапі, важливо зкопіювати результат білду до теки `crates/embedded-resources/static`, вміст якої статично додається до бінарного пакету `kellnr`: ``` bash cp -r dist/. ../crates/embedded-resources/static/ ``` Тепер збираємо корінь .rs: ``` bash cd .. cargo build --release ``` ## Запуск Перш, як почепити налаштований демон на systemd, службу можна запустити на локалхост: ``` bash target/release/kellnr start --registry-data-dir /path/to/kellnr \ --proxy-enabled true --local-ip 127.0.0.1 -l debug ``` Після запуску, зайшов до адмінки, вказавши логін `admin` і пароль `admin`: => http://127.0.0.1:8000/login?redirect=settings * на віддаленому сервері потрібно цей пароль змінити (в моїй версії була помилка не співпадіння - проігноруйте її, бо пароль насправді змінюється). Див. також `--setup-admin-pwd`. База даних тут стандартно SQLite, вона буде розташована за вказаним шляхом `/path/to/kellnr`. ## Підключення клієнта Щоб почати наповнення індексу запланованим мною способом (тобто через кешування проксі) додаю наступні рядки до клієнтського файлу `~/.cargo/config.toml`: ``` ~/.cargo/config.toml # Оголошуємо параметри підключення до сервера [registries.kellnr] index = "sparse+http://127.0.0.1:8000/api/v1/crates/" # Вказуємо Cargo замінити стандартне джерело crates.io на Kellnr [source.crates-io] replace-with = "kellnr" # Варто збільшити час очікування, оскільки трафік "блукатиме" значно довше [http] timeout = 180 ``` Тепер, якщо відкрити будь який проєкт Rust і виконати в ньому оновлення індексу, то отримаємо наступне: ``` bash $ cargo update Updating `kellnr` index error: no matching package named `libadwaita` found ``` Ця помилка відбувається тому, що ми "залінкувались" на статичне сховище, яке наразі порожнє. Тому в даному випадку, повертаємось до `~/.cargo/config.toml` і змінюємо `index` на URL з суфіксом `cratesio`: ``` bash index = "sparse+http://127.0.0.1:8000/api/v1/cratesio/" ``` Повторюємо оновлення і бачимо, що залежності почали відбудовуватись, а кеш - наповнюватись: => rust-crates-mirroring-with-kellnr/proxy-index-update.png Оновлення кешованого індексу проксі на головній сторінці Kellnr (скріншот) Інші способи глобального і локального оголошення джерела пакунків описані в документації, зупинятись окремо на кожному не будемо: => https://kellnr.io/documentation#configure-cargo ## Спільний сервер За схожою логікою, можна розгорнути суспільний сервіс, замінивши хост з `127.0.0.1` на `0.0.0.0` або `::`. Такий сервіс буде зручно запускати від окремого користувача `kellnr` через systemd: ``` bash useradd -mr kellnr ``` * в домашній теці - будуть зберігатись журнали і власне дані сервера Системний сервіс у мене виглядає наступним чином: ``` /etc/systemd/system/kellnr.service [Unit] After=network.target Wants=network.target [Service] Type=simple User=kellnr Group=kellnr ExecStart=/usr/local/bin/kellnr start --registry-data-dir /home/kellnr \ --proxy-enabled true \ # кешувати файли крейтів локально (до теки cratesio) # --proxy-download-on-update true \ --local-ip :: \ --local-port 8180 \ # в мене використовується багатомережний origin # --origin-host=.ua.srv \ # --origin-port=8180 \ # деталізація журналів -l warn Environment="NO_COLOR=1" StandardOutput=file:///home/kellnr/debug.log StandardError=file:///home/kellnr/error.log [Install] WantedBy=multi-user.target ``` * для Tor і I2P достатньо лише перекинути на будь-який IP "прихований сервіс" або "тунель" * теоретично, можна оголоситись на `::1` і перекинути проксі Nginx, але не знаю чи це працюватиме в контексті `sparse` Після цього відкривається порт: ``` bash ufw allow from 0200::/7 to 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148 port 8180 proto tcp ufw allow from 0400::/7 to 505:6847:c778:61a1:5c6d:e802:d291:8191 port 8180 proto tcp ``` * у прикладі використовуються дозволи для мереж Yggdrasil і Mycelium Нагадаю, що на серверах Fedora, потрібно окремо оголосити наступну політику SELinux: ``` bash setsebool -P httpd_can_network_connect 1 semanage port -a -t http_port_t -p tcp 8180 ``` * а дані профілю - розмістити в канонічних теках `/var/lib/kellnr` і `/var/log/kellnr` відповідно (з правами `kellnr:kellnr`) Керування сервісом відбувається класично: * `systemctl restart kellnr` - (пере) запуск * `systemctl enable kellnr` - авто-старт з системою * `systemctl status kellnr` - перевірка статусу --- Варто враховувати, що простір може швидко заповнитись, якщо проксі-сервер активно використовується. Стосовно перманентного хостингу проєктів - як правило, використовуються облікові записи і згенеровані для них токени, утім це вже інша тема. ### Налаштування вихідного проксі На моєму сервері немає Інтернет-інтерфейсу і я не можу забрати пакунки з crates.io напряму. Kellnr також поки не підтримує обгортку сокетів програмно, тому я створив відповідний тікет і згодом отримав відповідь з порадою використовувати змінні оточення (які підтримуються імплементацією https://crates.io/crates/reqwest) => https://github.com/kellnr/kellnr/issues/1096#issuecomment-3968553238 Змінні оточення звичайно вказуються через `export` або префіксом команди `kellnr`: ``` bash HTTP_PROXY=http://[::1]:8118 \ HTTPS_PROXY=http://[::1]:8118 \ NO_PROXY=localhost,127.0.0.1,::1,202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148,505:6847:c778:61a1:5c6d:e802:d291:8191 \ /usr/local/bin/kellnr ``` Я користуюсь systemd і його фрагмент `[Service]` виглядає так: ``` /etc/systemd/system/kellnr.service [Service] Environment="HTTP_PROXY=http://[::1]:8118" Environment="HTTPS_PROXY=http://[::1]:8118" Environment="NO_PROXY=localhost,127.0.0.1,::1,202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148,505:6847:c778:61a1:5c6d:e802:d291:8191" ``` * таким чином, будь-який запит здійснений процесом `kellnr` - буде перенаправлено через відповідний проксі, окрім запитів на мережі, вказані в `NO_PROXY` (у мене там адреси сервера Yggdrasil і Mycelium бо процес направлятиме на них деякі службові запити API) #### Проксування засобами Nginx Рішення нижче виявилось робочим лише частково: наприклад, завантажити крейт (`cargo install`) не вийде, тільки підтягнути до нього залежності (`cargo update`). Також цей сценарій чомусь не зберігає кешовані файли, тільки наповнює індекс в БД. Тому від описаного нижче способу я згодом відмовився бо він потребує доопрацювання вибіркових шляхів API через `/etc/hosts` і додаткових правил для config.json: => https://github.com/rust-lang/crates.io-index/blob/master/config.json Коротше, цей спосіб - костиль, при чому кривий, але я лишу його для історії, якщо вам з якихось причин не підходить спосіб зі змінними оточення або потрібен тюнінг окремих URI. ``` /etc/nginx/sites-available/default server { listen [::1]:8200; access_log /var/log/nginx/kellnr.access.log; error_log /var/log/nginx/kellnr.error.log warn; location / { proxy_pass http://[::1]:8118; proxy_set_header X-Forwarded-Proto "https"; proxy_set_header Host "index.crates.io"; } location /crates { proxy_pass http://[::1]:8118; proxy_set_header X-Forwarded-Proto "https"; proxy_set_header Host "static.crates.io"; } } ``` * `[::1]:8118` - це будь-який локальний проксі (типу privoxy або gost) який вміє обробляти HTTP * теоретично, можна пускати трафік через Tor, без проксі-посередника, але в моїй редакції Arti підтримується тільки SOCKS, а той - не підтримується Nginx Окремо, маю ще такий конфіг, який кидає запити напряму без маршрутизуючого проксі: ``` server { listen [::1]:8200; access_log /var/log/nginx/kellnr.access.log; error_log /var/log/nginx/kellnr.error.log warn; location / { proxy_set_header X-Forwarded-Proto "https"; proxy_set_header Host "index.crates.io"; proxy_ssl_server_name on; proxy_ssl_name "index.crates.io"; # dig index.crates.io proxy_pass https://151.101.238.137; } location /crates { proxy_set_header X-Forwarded-Proto "https"; proxy_set_header Host "static.crates.io"; proxy_ssl_server_name on; proxy_ssl_name "static.crates.io"; # dig static.crates.io proxy_pass https://151.101.238.137; } } ``` * на ньому я резольвлю руками `.crates.io` бо піймав якісь глюки Nginx: коли він слухає на IPv6, то резольвить на те ж сімейство адрес, інтерфейсу якого в мене немає і опції типу `resolver 8.8.8.8 8.8.4.4 ipv6=off valid=300s;` - не допомагають * можливо, деякі поля можна прибрати, але це остання заготовка яка працювала, тому додав "як є" Відповідно до нашого прикладу Nginx, при запуску сервера Kellnr, потрібно додати два аргументи: ``` bash kellnr start .. \ --proxy-index="http://[::1]:8200" \ --proxy-url="http://[::1]:8200/crates" ``` * тут я також пробував різні аналоги proxychains і "магічні" URL типу `http://[::1]:8118/index.crates.io/` - але цей фокус не пройшов, тому ось такий костиль з Nginx, який працює. ### Вирішення проблем Якщо на стороні клієнта бачите помилку: ``` bash $ cargo build --locked warning: spurious network error (3 tries remaining): [7] Could not connect to server (Failed to connect to 127.0.0.1 port 8000 after 1 ms: Could not connect to server) ``` то не плутайте її з локальними налаштуваннями `Cargo.toml` і вкажіть на сервері актуальний `--origin-*`, вирішивши спочатку проблему його підключення до вихідного проксі. Якщо помилки: ``` bash $ cargo build --locked warning: spurious network error (3 tries remaining): [28] Timeout was reached (Operation timed out after 30000 milliseconds with 0 bytes received) ``` збільшіть значення `http.timeout` в `~/.cargo/config.toml` ## Зауваження щодо безпеки Хоча в Cargo передбачається декілька рівнів перевірки цілісності пакетів, буде не зайвим переконатись в наявності оригінального файлу `Cargo.lock` для вашого проєкту, що містить хеші залежностей. Ці хеш-суми стандартно не доступні в реєстрах crates.io, утім вони звичайно зберігаються в репозиторіях Git. При використанні будь яких проксі, рекомендую збиратись з використанням аргументу `--locked`: ``` bash cargo build --locked ``` Додатково, для запобігання компрометації пакунків, варто створити окремий апстрім для індексів з хешами: наприклад на базі офіційного дзеркала GitHub, опціонально розгорнувши мульти-мережне дзеркало Radicle: => https://github.com/rust-lang/crates.io-index ``` crates.io > -|- out proxy > kellnr server > kellnr client > cargo crates.io-index > _↑ | ↑_________________[--proxy-index]__________________↓ ``` ## Посилання Наш експериментальний вузол в мережах Yggdrasil і Mycelium, побачимо що з того вийде: * `http://[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8180` | http://ygg.ua.srv:8180 * `http://[505:6847:c778:61a1:5c6d:e802:d291:8191]:8180` | http://myc.ua.srv:8180 Не офіційне дзеркало Kellnr в [Radicle](https://devzone.org.ua/post/radicle-detsentralizovanyy-p2p-khostynh-gitdvcs), знімок репозиторію на момент написання матеріалу: ``` rad:z4VnEyS5YXnFpEgY1iheHneRPeSX6 ```