Розгортання Веб-інфраструктури Radicle на прикладі оверлейних мереж


У попередньому гайді серії про децентралізований Git-хостинг Radicle, було розглянуто приклад налаштування публічного сіда для поширення персональних репозиторіїв в оверлейному режимі з політикою "Selective":

Розгортання сіда Radicle в мульти-мережному середовищі


Цього разу, опишу особистий досвід розгортання публічного Веб-інтерфейсу на його основі, для користувачів оверлейних IPv6 мереж Yggdrasil і Mycelium. Мотивація - зробити сідуючий сервер доступним для локальних користувачів, які бажають переглядати репозиторії "всередині" оверлейної мережі та мати можливість забирати код як з `rad clone` так і через шлюз HTTP звичною командою `git clone`.


Веб-інфраструктура Radicle ділиться на дві основні частини: сервер JSON/API (за яку відповідає пакунок `radicle-httpd`) і статичний асинхронний клієнт (на базі технологій HTML і JavaScript). Обидва рішення є частиною репозиторію `radicle-explorer`.


Отримання початкового коду


У попередньому гайді, на сервері вже було створено користувача `radicle`, тож спочатку залогінимось від нього:


su radicle

Оскільки на моєму сервері немає Інтернет-інтерфейсу як такого, але вже є підключений до глобальної мережі (засобами Tor over Yggdrasil) `radicle-node`, я буду тягнути вихідний код засобами команди `rad` а не `git` (для якого в моєму випадку знадобився б вихідний проксі). Це за одно дозволить звикнути до нової обгортки і скористатись перевагами пірингового обміну без прив'язки до конкретної мережі:


rad clone rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5 radicle-explorer

Якщо таки збираєте клієнт без локального вузла Radicle:


git clone https://iris.radicle.xyz/z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git radicle-explorer

Сервер JSON/API (radicle-httpd)


Інструкції з розгортання також описані в офіційній документації:

Radicle Seeder Guide: Running the HTTP Daemon


Якщо коротко, то робимо наступне:


cd radicle-explorer/radicle-httpd
cargo build --release

Копіюємо отриманий бінарник `target/release/radicle-httpd` до `/usr/local/bin` на сервері і переконуємось що користувач `radicle` має відповідні права на його виконання:


sudo chown radicle:radicle /usr/local/bin/radicle-httpd
sudo chmod +x /usr/local/bin/radicle-httpd

Системний сервіс


Сервіс я оголосив на локальному інтерфейсі `[::1]` (для IPv4 - це може бути `127.0.0.1`) з портом `8788` і подальшим проксуванням через Nginx з публічних IP відповідних мереж.


[Unit]
Description=Radicle HTTP Daemon
After=network.target network-online.target
Requires=network-online.target

[Service]
User=radicle
Group=radicle
ExecStart=/usr/local/bin/radicle-httpd --listen [::1]:8788
Environment=RAD_HOME=/home/radicle/.radicle RUST_BACKTRACE=1 RUST_LOG=info NO_COLOR=1
KillMode=process
Restart=always
RestartSec=1

StandardOutput=file:///home/radicle/httpd-debug.log
StandardError=file:///home/radicle/httpd-error.log

[Install]
WantedBy=multi-user.target

Додаємо сервіс до авто-запуску зі стартом системи і запускаємо сервер:


sudo systemctl enable --now radicle-httpd

Приклад конфігурації systemd в офіційному репозиторії


Nginx


server {
	listen [202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8788;
	listen [505:6847:c778:61a1:5c6d:e802:d291:8191]:8788;

	# Поки не визначився
	# server_name _;

	access_log /var/log/nginx/radicle.access.log;

	location / {
		proxy_pass http://[::1]:8788;

		proxy_http_version 1.1;
		proxy_set_header   Connection        "";

		proxy_set_header   Host              $host;
		proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
		proxy_set_header   X-Forwarded-Proto $scheme;
	}
}

Застосовуємо оновлення конфігурації:


systemctl reload nginx

Фаєрвол


sudo ufw allow from 0200::/7 to 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148 port 8788 proto tcp comment 'radicle-httpd'
sudo ufw allow from 0400::/7 to 505:6847:c778:61a1:5c6d:e802:d291:8191 port 8788 proto tcp comment 'radicle-httpd'

Тестування бекенду


В рамках цього прикладу, перевірити роботу JSON/API, можна за адресами:


http://[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8788

http://[505:6847:c778:61a1:5c6d:e802:d291:8191]:8788


Якщо встановлено Alfis DNS:


http://ygg.ua.srv:8788

http://myc.ua.srv:8788


По аналогії, на бекенд можна легко причепити тунелі I2P і Tor.


Клієнт (radicle-explorer)


По суті, це статичний Веб-компонент на базі HTML/JavaScript, що звертається до вказаного в його налаштуваннях сервера. Оптимізована статика збирається засобами пакетного менеджера `npm`, після чого отримані файли з теки `build` копіюються до публічного простору, наприклад Nginx: `/var/www/radicle`.


Налаштування підключення до бекенду


Перед тим, як збирати оптимізований білд, важливо спочатку вказати актуальні налаштування підключення до серверів JSON/API - власних або сторонніх. Робиться це у файлі `config/default.json`. Після збірки, ці налаштування будуть "вбудовані" в компонент `build/assets/components-xxx.js` і при наступних оновленнях конфігурації, потрібно буде перезбиратись. Щоб уникнути цієї незручності, можна використовувати опцію динамічних налаштувань `VITE_RUNTIME_CONFIG=true`, про яку детальніше описано у розділі "Компіляція".


У своїй конфігурації, поки використовую два інтерфейси: Yggdrasil і Mycelium свого сіда. По аналогії, до масиву об'єктів `preferredSeeds` додаються й альтернативні DNS, тунелі I2P або приховані сервіси Tor.


Важливим є той факт, що поточна версія `radicle-explorer` ніяк не проксує через бекенд `preferredSeeds` і тому якщо в клієнта не встановлено роутер Mycelium, то перебуваючи на хості Yggdrasil - він не зможе з меню `0200::/7` відкрити сід `0400::/7`. Навожу приклад міксованого з'єднання, але рекомендую користуватись одним сімейством адрес для одного фронтенду.


{
  "nodes": {
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
    "requiredApiVersion": "~0.18.0",
    "defaultHttpdPort": 443,
    "defaultLocalHttpdPort": 8080,
    "defaultHttpdScheme": "http"
  },
  "source": {
    "commitsPerPage": 30
  },
  "supportWebsite": "https://radicle.zulipchat.com",
  "deploymentId": null,
  "preferredSeeds": [
    {
      "hostname": "[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]",
      "port": 8788,
      "scheme": "http"
    },
    {
      "hostname": "ygg.ua.srv",
      "port": 8788,
      "scheme": "http"
    },
    {
      "hostname": "[505:6847:c778:61a1:5c6d:e802:d291:8191]",
      "port": 8788,
      "scheme": "http"
    },
    {
      "hostname": "myc.ua.srv",
      "port": 8788,
      "scheme": "http"
    }
  ]
}

Стандартні поля секції `nodes` - свідомо не змінюю (окрім встановлення стандартної схеми `http`), бо в коді клієнта є такі упороті моменти:


$: portFragment =
  baseUrl.scheme === config.nodes.defaultHttpdScheme &&
  baseUrl.port === config.nodes.defaultHttpdPort
    ? ""
    : `:${baseUrl.port}`;

Зауваження щодо HTTP


Якщо відкрити Веб-інтерфейс Radicle на віддаленому (не `localhost`) сервері з протоколом HTTP, то при копіюванні адрес репозиторію до буферу - в консолі браузера буде помилка:


Uncaught (in promise) TypeError: can't access property "writeText", navigator.clipboard is undefined


Вона означає, що сучасна політика браузера блокує функціональність буферу копіювання для не захищеного протоколу HTTP, за виключенням `localhost` або при ручному додаванні такого виключення через `about:config`. Звісно, для продакшну це не варіант, бо переважна більшість користувачів через захищену природу оверлейних мереж, не користуються HTTPs і вважають таке явище - швидше не правильно налаштованим Nginx, аніж фічею.


Отже, якщо не плануєте "силою" заганяти юзерів на HTTPs (з само-підписаним сертифікатом) то рішення я знайшов тільки у застосуванні патчу `src/lib/utils.ts`, із заміною його функції `toClipboard` на "legacy-fallback" через `copyToClipboard`:


async function copyToClipboard(text: string) {
  if (navigator.clipboard && window.isSecureContext) {
    await navigator.clipboard.writeText(text);
  } else {
    const textArea = document.createElement("textarea");
    textArea.value = text;
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      document.execCommand('copy');
    } catch (err) {
      console.error('Unable to copy', err);
    }
    document.body.removeChild(textArea);
  }
}
export async function toClipboard(text: string): Promise<void> {
  await copyToClipboard(text);
}

Компіляція


Після завершення конфігурації, на локальній машині збираємо оптимізовану статику і для зручності передачі на сервер, запаковуємо вміст `build` до архіву `radicle-explorer.tar.gz`:


cd radicle-explorer
npm install
VITE_RUNTIME_CONFIG=true npm run build
tar -czvf radicle-explorer.tar.gz -C build .

https://app.radicle.xyz/nodes/iris.radicle.xyz/rad%3Az4V1sjrXqjvFdnCUbxPFqd5p4DtH5/tree/README.md#run-time-configuration


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


Отриманий архів `radicle.tar.gz` копіюємо на сервер до якоїсь тимчасової теки і розпаковуємо вміст архіву за призначенням `/var/www/radicle`, оновивши за одно права для доступу служби Nginx:


sudo mkdir -p /var/www/radicle
sudo tar -xzvf /tmp/radicle-explorer.tar.gz -C /var/www/radicle
sudo rm /tmp/radicle-explorer.tar.gz
sudo chown www-data:www-data /var/www/radicle

Nginx


Тут все просто: вказуємо актуальний шлях до статики `/var/www/radicle` з адресацією усіх запитів на `index.html`:


server {
	listen [202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8780;
	listen [505:6847:c778:61a1:5c6d:e802:d291:8191]:8780;

	# Ще не визначився
	# server_name _;

	access_log /var/log/nginx/radicle.access.log;
	root /var/www/radicle;

	location / {
		try_files $uri $uri/ /index.html;
	}

	# Якщо збірка з `VITE_RUNTIME_CONFIG=true`
	#
	# location = /config.json {
	#	root /;
	# 	try_files /etc/radicle-explorer/config.json /usr/share/radicle-explorer/config.json =404;
	# }

	# Цей блок є опціональним і дозволяє публікувати `radicle-httpd` і `radicle-explorer`
	# на одному інтерфейсі одночасно (на прикладі це може спільний порт `8780`)
	# таким чином, індексна сторінка API буде заміщена користувацьким Веб-інтерфейсом
	#
	# location ~ "^/(raw|api|[a-zA-Z0-9]{28,29}(\.git)?)(/|$)" {
	# 	proxy_pass http://[::1]:8788;
	#
	# 	proxy_http_version 1.1;
	# 	proxy_set_header   Connection        "";
	#
	# 	proxy_set_header   Host              $host;
	# 	proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
	# 	proxy_set_header   X-Forwarded-Proto $scheme;
	# }
}

Застосовуємо оновлення конфігурації:


systemctl reload nginx

Фаєрвол


sudo ufw allow from 0200::/7 to 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148 port 8780 proto tcp comment 'radicle-explorer'
sudo ufw allow from 0400::/7 to 505:6847:c778:61a1:5c6d:e802:d291:8191 port 8780 proto tcp comment 'radicle-explorer'

Тестування фронтенду


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


http://[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8780

http://[505:6847:c778:61a1:5c6d:e802:d291:8191]:8780


Якщо встановлено Alfis DNS:


http://ygg.ua.srv:8780

http://myc.ua.srv:8780


Анонімні мережі


Веб сервіс Radicle є цілком сумісним з мережами на базі анонімних проксі I2P і Tor. Нюансом є лише обмеження політики CORS у випадку, якщо спробуєте відвідати сіди I2P/Tor перебуваючи на сервері наприклад IPv6. Але якщо підняти окрему копію експлорера без "солянки", то користувачі зможуть переглядати та клонувати репозиторії анонімно.


Особисто я використовую наступну структуру файлової системи з релевантними для кожної теки файлами `config.json`:


mkdir -p /var/www/radicle/yggdrasil \
         /var/www/radicle/i2p \
         /var/www/radicle/mycelium \
         /var/www/radicle/tor

I2P


На прикладі i2pd, до конфігурації тунелів додаються два HTTP сервери:


[radicle-explorer]
type = http
host = ::1
port = 8780
inport = 80
keys = radicle-explorer.dat

[radicle-api]
type = http
host = 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148
port = 8788
inport = 8788
keys = radicle-api.dat

Після перезапуску роутера i2pd, засобами i2pd-tools отримуються згенеровані адреси B32:


$ keyinfo /var/lib/i2pd/radicle-explorer.dat
k7cfad745uretan7iihkwo6x24ut6mgbhq4ccxjqkzetgbtfknbq.b32.i2p

$ keyinfo /var/lib/i2pd/radicle-explorer.dat
cfwfe2k6dropbymtddz225mbugzs5tfsmvng23zsebf6iw3cj2xa.b32.i2p

Додаємо адресу `radicle-api` до `preferredSeeds`:


"preferredSeeds": [
  {
    "hostname": "cfwfe2k6dropbymtddz225mbugzs5tfsmvng23zsebf6iw3cj2xa.b32.i2p",
    "port": 8788,
    "scheme": "http"
  }
]

Перевіряємо:

http://k7cfad745uretan7iihkwo6x24ut6mgbhq4ccxjqkzetgbtfknbq.b32.i2p


Tor


Я користуюсь більш сучасною реалізацією роутера - Arti, детальніше про нього було написано тут:

Встановлення Onion-роутера Arti з підключенням до мережі Tor через Yggdrasil


Підходів налаштування "прихованого сервісу" для Radicle може буде декілька. Можна створити окремі `onion_services` на `80` порт, але я поки використовую підхід зі спільним доменом на різних портах:


[onion_services."radicle"]
#enabled = true
proxy_ports = [
    # radicle-explorer (WebUI)
    ["80", "[::1]:8781"],
    # radicle-httpd (JSON/API)
    ["8788", "[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8788"],
    # radicle-node (public seed)
    # ["8776", "[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8776"],
    ["*", "destroy"]
]

Розгортання сіда Radicle в мульти-мережному середовищі


Після збереження налаштувань і перезапуску роутера, для отримання адреси `.onion` виконуємо команду:


$ su arti -s /bin/bash \
          -c 'arti hss -c /path/to/config.toml --nickname radicle onion-address'
...
tus2sol3kcykzh6dw4adgma3yzvzex7nsizlbdjmizgeines74chk7yd.onion

І додаємо результат до конфігурації експлорера у відповідному неймспейсі Nginx:


"preferredSeeds": [
  {
    "hostname": "tus2sol3kcykzh6dw4adgma3yzvzex7nsizlbdjmizgeines74chk7yd.onion",
    "port": 8788,
    "scheme": "http"
  }
]

Пробуємо підключитись і отримати приклади команд на клонування репозиторіїв по HTTP:


http://tus2sol3kcykzh6dw4adgma3yzvzex7nsizlbdjmizgeines74chk7yd.onion


Проксі для клієнта Git


Використання I2P і Tor - передбачає підключення клієнтських застосунків через проксі локального або віддаленого роутера. Це стосується й команди `git`. Щоб клонувати репозиторії Radicle засобами HTTP на прикладі I2P, потрібно до профілю Git додати наступний рядок:


git config http.proxy http://127.0.0.1:4444

Таким чином, клонування репозиторію `heartwood` з цього інстансу відбувається командою:


git clone http://cfwfe2k6dropbymtddz225mbugzs5tfsmvng23zsebf6iw3cj2xa.b32.i2p:8788/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git heartwood

Відповідно, користувачі Tor - оперуватимуть вже адресами `.onion`, використовуючи проксі з портом `9150` або `9050`, в залежності від обраного ними роутера:


git clone http://tus2sol3kcykzh6dw4adgma3yzvzex7nsizlbdjmizgeines74chk7yd.onion:8788/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git heartwood


/uk/