Організація локального дзеркала залежностей Cargo з Kellnr


Давно задумуюсь про організацію локального дзеркала залежностей crates.io, щоб не ходити за ними в Інтернет. При чому, я не маю достатньо простору для хостингу повної копії репозиторію, утім готовий ділитися тими крейтами, якими користуюся сам.


Трішки прозондувавши тему, віднайшов проєкт Kellnr:

https://kellnr.io


Це рішення позиціонується як Self Hosted Solution для хостингу крейтів, та головне - має фічу вбудованого кешуючого проксі, що власне під мої задачі підходить.


Дане рішення постачається з Веб-інтерфейсом для адміністрування: зокрема, керування правами доступу.


Головна сторінка каталогу (скріншот)


Сторінка пошуку містить відповідну перемичку фільтрації:


Фільтрація кешованих пакунків в Kellnr (скріншот)


Отже, має бути зручно, особливо для візуалізації результатів CLI-експериментів. Нижче занотую процес встановлення і налаштування публічної ноди (бо варіант з Docker мені не підходить як і автоматичне встановлення скриптом `.sh` - якщо пускатиму в контейнері, то робитиму це руками в LXC)


Встановлення


git clone https://github.com/kellnr/kellnr.git
cd kellnr

Якщо планується компіляція кореневого `Cargo.toml` то потрібно спочатку зібрати компонент `ui`, інакше буде помилка на етапі `crates/embedded-resources`:


cd ui
npm install
npm run build

На цьому етапі, важливо зкопіювати результат білду до теки `crates/embedded-resources/static`, вміст якої статично додається до бінарного пакету `kellnr`:


cp -r dist/. ../crates/embedded-resources/static/

Тепер збираємо корінь .rs:


cd ..
cargo build --release

Запуск


Перш, як почепити налаштований демон на systemd, службу можна запустити на локалхост:


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



База даних тут стандартно SQLite, вона буде розташована за вказаним шляхом `/path/to/kellnr`.


Підключення клієнта


Щоб почати наповнення індексу запланованим мною способом (тобто через кешування проксі) додаю наступні рядки до клієнтського файлу `~/.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 і виконати в ньому оновлення індексу, то отримаємо наступне:


$ cargo update
    Updating `kellnr` index
error: no matching package named `libadwaita` found

Ця помилка відбувається тому, що ми "залінкувались" на статичне сховище, яке наразі порожнє. Тому в даному випадку, повертаємось до `~/.cargo/config.toml` і змінюємо `index` на URL з суфіксом `cratesio`:


index = "sparse+http://127.0.0.1:8000/api/v1/cratesio/"

Повторюємо оновлення і бачимо, що залежності почали відбудовуватись, а кеш - наповнюватись:


Оновлення кешованого індексу проксі на головній сторінці Kellnr (скріншот)


Інші способи глобального і локального оголошення джерела пакунків описані в документації, зупинятись окремо на кожному не будемо:

https://kellnr.io/documentation#configure-cargo


Спільний сервер


За схожою логікою, можна розгорнути суспільний сервіс, замінивши хост з `127.0.0.1` на `0.0.0.0` або `::`.


Такий сервіс буде зручно запускати від окремого користувача `kellnr` через systemd:


useradd -mr kellnr

Системний сервіс у мене виглядає наступним чином:


[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-hostname .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

Після цього відкривається порт:


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

Нагадаю, що на серверах Fedora, потрібно окремо оголосити наступну політику SELinux:


setsebool -P httpd_can_network_connect 1
semanage port -a -t http_port_t -p tcp 8180

Керування сервісом відбувається класично:



---


Варто враховувати, що простір може швидко заповнитись, якщо проксі-сервер активно використовується.


Стосовно перманентного хостингу проєктів - як правило, використовуються облікові записи і згенеровані для них токени, утім це вже інша тема.


Налаштування вихідного проксі


На моєму сервері немає Інтернет-інтерфейсу і я не можу забрати пакунки з crates.io напряму. Kellnr також поки не підтримує обгортку сокетів програмно, тому я створив відповідний тікет і згодом отримав відповідь з порадою використовувати змінні оточення (які підтримуються імплементацією https://crates.io/crates/reqwest)

https://github.com/kellnr/kellnr/issues/1096#issuecomment-3968553238


Змінні оточення звичайно вказуються через `export` або префіксом команди `kellnr`:


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]` виглядає так:


[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"

# Проксування засобами Nginx


Рішення нижче виявилось робочим лише частково: наприклад, завантажити крейт (`cargo install`) не вийде, тільки підтягнути до нього залежності (`cargo update`). Також цей сценарій чомусь не зберігає кешовані файли, тільки наповнює індекс в БД.


Тому від описаного нижче способу я згодом відмовився бо він потребує доопрацювання вибіркових шляхів API через `/etc/hosts` і додаткових правил для config.json:

https://github.com/rust-lang/crates.io-index/blob/master/config.json


Коротше, цей спосіб - костиль, при чому кривий, але я лишу його для історії, якщо вам з якихось причин не підходить спосіб зі змінними оточення або потрібен тюнінг окремих URI.


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";
	}
}

Окремо, маю ще такий конфіг, який кидає запити напряму без маршрутизуючого проксі:


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;
	}
}

Відповідно до нашого прикладу Nginx, при запуску сервера Kellnr, потрібно додати два аргументи:


kellnr start .. \
        --proxy-index="http://[::1]:8200" \
        --proxy-url="http://[::1]:8200/crates"

Вирішення проблем


Якщо на стороні клієнта бачите помилку:


$ 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-*`, вирішивши спочатку проблему його підключення до вихідного проксі.


Якщо помилки:


$ 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`:


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]__________________↓

Не верифіковане проксі Kellnr на базі власного індексу


Посилання


Наш експериментальний вузол в мережах Yggdrasil і Mycelium, побачимо що з того вийде:



Не офіційне дзеркало Kellnr в Radicle:


rad:z4VnEyS5YXnFpEgY1iheHneRPeSX6

Radicle: децентралізований P2P хостинг Git/DVCS


Дивіться також


Спільне сховище Kellnr на флешці USB



/uk/