LINUX.ORG.RU

Сообщения KivApple

 

Отказоустойчивость

Вот я понимаю как делать отказоустойчивость с одной стороны. Несколько инстансов бекэнда, несколько реплик БД. Бекэнд можно написать stateless, БД существуют распределённые. Одна нода упадёт, другие ноды возьмут нагрузку.

Но в конечном счёте что бы мы не навернули в инфраструктуре, в DNS записи придётся указать один конкретный IP адрес. И сервер, чей это IP станет единой точкой отказа, хоть он проксирует трафик на 10 бекэндов, настроен проверять в фоне что они живы и исключать из round robin упавшие ноды.

Насчёт «один IP адрес» я, конечно, слукавил, но не сильно - несколько A/AAAA записей с одинаковым именем можно использовать только для балансировки нагрузки, но не для отказоустойчивости (клиенты выбирают случайный IP из списка, но не ретраят другой, если не фортанёт).

При этом DNS имеет TTL и обновляется не быстро - даже 5 минут дайнтайма для крупного ресурса будет ощутимо, а с учётом того что некоторые DNS сервера кешируют дольше указанного TTL, речь может идти и о часах.

И как же тогда обеспечивают настоящую отказоустойчивость, чтобы реверс прокси тоже можно было ронять/перезапускать?

Пока на ум приходит только CDN типа CloudFlare, которые позволяют переключать IP адрес записи гораздо быстрее, чем позволяет DNS (так как DNS не обновляется, обновляются лишь настройки их сообственного реверс прокси и там уже время реакции секунды).

 

KivApple
()

ESP32 + HTTPS + CloudFlare

Хочу подключиться по веб-сокету к своему бекэнду с ESP32-C3. Бекэнд сидит за CloudFlare.

#include "esp_websocket_client.h"
#include "esp_crt_bundle.h"

...

esp_websocket_client_config_t cfg = {
	.uri = "wss://subdomain.example.com/api/ws",
	.crt_bundle_attach = esp_crt_bundle_attach
};
ws_client = esp_websocket_client_init(&cfg);
esp_websocket_register_events(
	ws_client,
	WEBSOCKET_EVENT_ANY,
	websocket_event_handler,
	NULL
);
esp_websocket_client_start(ws_client);

Получаю вот что:

E (34431) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x2700
E (34431) esp-tls: Failed to open new connection
E (34431) transport_base: Failed to open a new connection
E (34441) transport_ws: Error connecting to host subdomain.example.com:443
E (34441) websocket_client: esp_transport_connect() failed with -1, transport_error=ESP_ERR_MBEDTLS_SSL_HANDSHAKE_FAILED, tls_error_code=9984, tls_flags=0, esp_ws_handshake_status_code=0, errno=119

Попробовал использовать наиболее актуальный бандл корневых сертификатов скачав его отсюда https://curl.se/docs/caextract.html и подложив в конфигурацию SDK (конфигурация включенных алгоритмов шифрования и т. п. оставлена по умолчанию, там включено почти всё):

CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="cacert-2025-11-04.pem"
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=300
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y

Не помогло. Та же ошибка, что и со стандартным бандлом из ESP-IDF.

На ESP32-C3 включен NTP и часы показывают правильное время.

С компьютера из той же Wi-Fi сети мой сайт открывается (+ я не в России и здесь CloudFlare никто не блокирует).

Проверил сертификаты сайта:

echo | openssl s_client -connect subdomain.example.com:443 -showcerts

Connecting to 2a06:98c1:3121::6
CONNECTED(00000005)
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R4
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WE1
verify return:1
depth=0 CN=example.com
verify return:1
---
Certificate chain
 0 s:CN=example.com
   i:C=US, O=Google Trust Services, CN=WE1
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Nov  4 XX:XX:XX 2025 GMT; NotAfter: Feb  2 XX:XX:XX 2026 GMT
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
 1 s:C=US, O=Google Trust Services, CN=WE1
   i:C=US, O=Google Trust Services LLC, CN=GTS Root R4
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Dec 13 09:00:00 2023 GMT; NotAfter: Feb 20 14:00:00 2029 GMT
-----BEGIN CERTIFICATE-----
MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw
MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp
Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+
Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP
LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F
avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku
Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv
ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx
AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V
sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN
7uJW
-----END CERTIFICATE-----
 2 s:C=US, O=Google Trust Services LLC, CN=GTS Root R4
   i:C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
   a:PKEY: id-ecPublicKey, 384 (bit); sigalg: RSA-SHA256
   v:NotBefore: Nov 15 03:43:21 2023 GMT; NotAfter: Jan 28 00:00:42 2028 GMT
-----BEGIN CERTIFICATE-----
MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX
MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE
CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIzMTEx
NTAzNDMyMVoXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT
GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE83Rzp2iLYK5DuDXFgTB7S0md+8Fhzube
Rr1r1WEYNa5A3XP3iZEwWus87oV8okB2O6nGuEfYKueSkWpz6bFyOZ8pn6KY019e
WIZlD6GEZQbR3IvJx3PIjGov5cSr0R2Ko4H/MIH8MA4GA1UdDwEB/wQEAwIBhjAd
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd
BgNVHQ4EFgQUgEzW63T/STaj1dj8tT7FavCUHYwwHwYDVR0jBBgwFoAUYHtmGkUN
l8qJUC99BM00qP/8/UswNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRw
Oi8vaS5wa2kuZ29vZy9nc3IxLmNydDAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8v
Yy5wa2kuZ29vZy9yL2dzcjEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqG
SIb3DQEBCwUAA4IBAQAYQrsPBtYDh5bjP2OBDwmkoWhIDDkic574y04tfzHpn+cJ
odI2D4SseesQ6bDrarZ7C30ddLibZatoKiws3UL9xnELz4ct92vID24FfVbiI1hY
+SW6FoVHkNeWIP0GCbaM4C6uVdF5dTUsMVs/ZbzNnIdCp5Gxmx5ejvEau8otR/Cs
kGN+hr/W5GvT1tMBjgWKZ1i4//emhA1JG1BbPzoLJQvyEotc03lXjTaCzv8mEbep
8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1
vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl
-----END CERTIFICATE-----
---
Server certificate
subject=CN=example.com
issuer=C=US, O=Google Trust Services, CN=WE1
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 2814 bytes and written 402 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 256 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
DONE

Взял второй сертификат из цепочки:

 1 s:C=US, O=Google Trust Services, CN=WE1
   i:C=US, O=Google Trust Services LLC, CN=GTS Root R4
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Dec 13 09:00:00 2023 GMT; NotAfter: Feb 20 14:00:00 2029 GMT
-----BEGIN CERTIFICATE-----
MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw
MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp
Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+
Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP
LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F
avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku
Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv
ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx
AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V
sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN
7uJW
-----END CERTIFICATE-----

Сохранил его в отдельный файл и прогнал верификацию на компьютере по используемому бандлу:

openssl verify -CAfile cacert-2025-11-04.pem cert.pem 
cert.pem: OK

То есть бандл позволяет верифицировать гугловский сертификат, которым уже подписан сертификат моего домена. Но ESP32 почему-то не может подключиться.

Пробовал включать-отключать TLSv1.3, ничего не изменилось. Пробовал включать weak domain validation в конфигурации SDK (немного смутило, что я подключаюсь к subdomain.example.com, а сертификат выдан на example.com, но как я понимаю это wildcard и всё ок), тоже не помогло.

В чём может быть проблема?

P.S.: Разумеется, я подключаюсь не к example.com, а к своему домену, просто заменил везде его.

 , ,

KivApple
()

Идея проекта веб-сервера

Короче, суть такова:

- Веб-сервер с поддержкой Let's encrypt и нескольких доменов

- Можно настраивать через API и через веб-интерфейс

- Можно деплоить (добавлять/удалять) статические файлы через API

- Транзакционный деплой - добавление и удаление файлов не видимо снаружи, пока не дёрнешь API коммита, после него видимы сразу все изменения

- Опционально можно задать апстрим (всего домена или отдельного роута или каталога). Если нет статического файла по пути, то запрос уходит в апстрим при наличии

- Опционально можно включить кеширование ответов апстрима - ответ на GET запрос без заголовков запрещающих кеширование сохраняется как статический файл (и в следующий раз запрос уже не пойдёт в апстрим)

- Статическим файлам можно задавать теги и есть API удаления сразу всех файлов по тегу (разумеется, атомарно)

Use-case:

- Деплой фронтэндов (вместо того чтобы руками таскать файлы на сервер или деплоить контейнер с NGINX можно грузить файлы одной командой по API и атомарно заменять)

- Кеш перед CMS (проставляем страницам теги таким образом, чтобы можно было за один вызов API удалять страницы, которые затронуло изменение и они были перезапрошены)

Как я понимаю, такое существует только в виде SaaS (Cloudflare Pages и т. д.), а OpenSource self-hosted нет.

Мне просто надоело свои фронтэнд проекты паковать в Docker с nginx с дефолтной конфигурацией.

Что думает LOR? Не нужно?

 

KivApple
()

mojiverse

Запилил гибрид игры «Жизнь» и всяких коллективных онлайн рисовалок типа pxls.space под соусом эмоджи.

Используются классические правила - если у эмоджи 2-3 соседа, он живёт, если меньше, умирает от одиночества, если больше, от перенаселения. Если у пустой клетки есть 3 соседа эмоджи, то там спаунится новый эмоджи (выбирается по большинству соседей).

Для большего интереса центр поля защищён от изменений игроками, но вы можете строить глайдеры и ружья по краям, которые будут проникать в центр.

Для защиты от вайпов и прочих злоупотреблений количество эмоджи, которое можно поставить постепенно растёт по мере того как вкладка с игрой активна и в фокусе.

Бек написан на Rust, фронт на TypeScript, крутится всё это на подкроватном сервере на Ubuntu. И играть в это, очевидно, тоже можно из-под Linux, если имеется в наличии любой современный браузер.

Ссылка: https://mojiverse.art/

 mojiverse, игра жизнь, пет проект

KivApple
()

«Три в ряд» с античитом

Предупреждаю, мне хочется очень странного.

Я хочу алгоритм генерации поля «три в ряд» (как начального состояния, так и заполнения новыми фишками, когда игрок сделал ход). На игровом поле есть обычные фишки и есть бонусы. Игрок получает отдельный очки за удаление обычных фишек и за удаление бонусов. Важна именно сумма собранных бонусов, очки за обычные фишки не принципиальны.

При этом алгоритм генерации должен быть особым. А именно: вначале игры с сервера получается некий seed поля (это может быть что-угодно, хоть один Long, хоть несколько килобайт данных) и затем используется. При этом сервер заранее знает, сколько максимум можно собрать бонусов на поле за всю партию (есть ограничение по времени, а зная длительности анимаций ходов, можно пересчитать это и в ограничение по ходам за партию). Например, он сначала генерирует количество бонусов, а уже потом создаёт seed из него.

Клиент использует seed для игры. В конце партии отправляет запрос на сервер. Можно отправлять сразу посчитанную сумму бонусов, можно ещё и весь набор ходов. А сервер валидирует результат, что сумма бонусов не ниже минимально возможной, не выше максимально возможной для этого seed (он же знает с какими параметрами генерировал seed).

При этом клиент не должен иметь возможности (должно быть вычислительно сложной задачей) по seed алгоритмически вычислить минимальное и максимальное число очков. Нужно по-честному играть.

Например, можно обеспечить это дорогим алгоритмом регенерации поля после хода. Таким образом, чтобы нельзя было перебрать все варианты на много шагов вперёд и найти оптимальный. Можно также как-то по-особому генерировать поле, чтобы плодить варианты и их было слишком много перебирать.

Можно немного модифицировать геймплей самого «Три в ряд» (вводить фишки с особыми эффектами, увеличивающими неопределённость), если это необходимо для достижения цели.

Короче, надо защититься от потенциального читерства а-ля «расковыряли API и просто спамим сервер запросами о том, что набрали максимум бонусов».

Возможно ли такое хотя бы теоретически? В какую сторону копать?

 ,

KivApple
()

Docker Overlay Network + Tailscale VPN

Есть несколько серверов объединённых в одну виртуальную сеть с помощью Tailscale VPN. На них установлен Docker и все сервера объединены в Docker Swarm, при этом от Docker Swarm используются лишь overlay network, без всего остального - контейнеры запускаются обычным docker compose.

Есть несколько overlay network созданных вручную с флагом attachable. В compose эти сети прописаны как external и некоторые контейнеры их используют.

На двух серверах из трёх всё работает хорошо - но там меньше десятка контейнеров на каждом. А вот на третьем сервере контейнеров 25 (и чуть ли не половина из них подключена к той или иной overlay network).

При перезагрузке стабильно не могут запустится (они запускаются за счёт restart=unless-stopped) 2-3 контейнера из числа тех, которые подключены к overlay network. Каждый раз это разные контейнеры.

Статус у них:

failed to set up container networking: attaching to network failed, make sure your network options are correct and check manager logs: context deadline exceeded

Если вручную сделать им docker compose up, они успешно стартуют.

Но мне то нужна автоматика - сервер должен сам поднять все сервисы в рабочее состояние после ребута.

В данный момент единственная кастомизация докера - добавление ему зависимости от tailscale в systemd юнит:

$ cat /etc/systemd/system/docker.service.d/override.conf 
[Unit]
Requires=tailscaled.service network-online.target
After=tailscaled.service network-online.target

[Service]
ExecStartPre=/bin/sh -c 'until ip link show tailscale0 >/dev/null 2>&1; do echo "Waiting for tailscale0 interface..."; sleep 1; done; until [ "$(tailscale status --json | jq -r .BackendState)" = "Running" ]; do echo "Waiting for Tailscale..."; sleep 1; done'

Это решило проблему, что зависимые от оверлейной сети контейнеры вообще не стартовали, но осталась проблема, что парочка случайных контейнеров не может стартовать на сервере, где их много.

Проблема усугубляется тем, что ошибка создания контейнера не считается его падением и restart не срабатывает.

UPD: Вроде решил с помощью:

$ cat /etc/systemd/system/docker.service.d/override.conf 
[Unit]
Requires=tailscaled.service network-online.target
After=tailscaled.service network-online.target

[Service]
ExecStartPre=/bin/sh -c 'until ip link show tailscale0 >/dev/null 2>&1; do echo "Waiting for tailscale0 interface…"; sleep 1; done; until [ "$(tailscale status --json | jq -r .BackendState)" = "Running" ]; do echo "Waiting for Tailscale…"; sleep 1; done'
ExecStartPost=/bin/bash -c 'sleep 30 && docker start $(docker ps -a -q --filter status=exited --filter status=created)'

Но непонятно насколько надёжно. Нет ли решений лучше.

 ,

KivApple
()

SwarmChatBot

Вдохновился Посоветуйте идею для TG-бота, которая минимум полезна и максимум можно продать как успеховую. и запилил за сегодняшний вечер небольшого Telegram бота.

Задумка проста. Добавляете бота в групповой чат. Пишете сообщение тегая бота (он не имеет доступа к сообщениям). Сообщение улетает в другой случайно выбранный чат, куда кто-то другой добавил этого бота.

Если кто-то в этом чате реплайнет на это сообщение, то ответ будет переслан назад в чат отправителя. Если зареплаить реплай, то реплай уйдёт в чат получателя и так далее. То есть поддерживается полноценная нить диалога между двумя чатами, но скрывается собственно какие это два чата и имена собеседников (и так как это чаты групповые, то отвечать могут разные люди знакомые между собой с одного конца).

В принципе бот работает и в личке, но моя больная фантазия рисует что-то типа, мол в групповых чатах должно быть веселее.

На случай если взлетит сделал систему хештегов - вместо приёма совсем рандома можно принимать сообщения содержащие некие хештеги, заданные в настройках чата.

Бот написан на Rust с Teloxide и запущен на сервере под убравлением Ubuntu.

UPD: Бот переименован

 

KivApple
()

Использование UUIDv7 в качестве токена авторизации

UUID v4 не рекомендуется использовать в качестве токенов авторизации: https://security.stackexchange.com/questions/157270/using-v4-uuid-for-authent...

Do not assume that UUIDs are hard to guess; they should not be used as security capabilities (identifiers whose mere possession grants access), for example. A predictable random number source will exacerbate the situation.

Как я понимаю, идея в том, что обычно для их генерации используется не криптостойкие источники рандома и поэтому их может оказаться легко угадать.

Но тем не менее я даже UUID v4 не хочу использовать, не то что криптостойкий рандом в качестве токена авторизации - я буду хранить токены в Postgres, а Postgres, поговаривают, плохо работает с UNIQUE колонками со случайными данными - B-дерево распухает.

Хочу использовать в качестве токенов авторизации UUID v7 - они должны быть по заверению авторов дружелюбны к индексам баз данных. Но беда в том, что их ещё проще угадать, ибо там половина бит даже не рандом неизвестного качества, а вообще метка времени.

Но что если в БД хранить UUID, а юзеру отдавать и от юзера принимать JWT, где UUID хранить в JTI (ну и срок жизни токена, если надо, в exp зашить, stateless фичами JWT мы пользоваться всё равно не будем, нам от него по сути только цифровая подпись токена нужна).

Теперь злоумышленнику мало угадать UUID, ему ещё надо его подписать секретом приложения, который он не знает (в качестве небольшого бонуса - если утечёт дамп БД - без конфига приложения он всё равно не позволит угнать сессии). И наоборот, если утёк секрет JWT, чтобы увести сессию надо ещё угадать UUID (что не невозможно, но всё равно требует усилий). С другой стороны, приложение выборку из БД делает по jti, который UUID v7, который хорошо дружит с индексами БД.

Что думаете о такой схеме?

 ,

KivApple
()

Rust + WGPU + dyn trait

Есть небольшой рабочий набросок кода, который комплируются и запускается, создавая окно winit и instance + surface wgpu:

use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::{Window, WindowId};

trait Renderer<'window> {
}

struct WGPURenderer<'window> {
	instance: wgpu::Instance,
	surface: wgpu::Surface<'window>
}

impl<'window> WGPURenderer<'window> {
	async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
		let instance = wgpu::Instance::default();
		let surface = instance.create_surface(Arc::clone(&window))?;
		Ok(Self {
			instance,
			surface
		})
	}
}

impl<'window> Renderer<'window> for WGPURenderer<'window> {
}


#[derive(Default)]
struct App<'window> {
	window: Option<Arc<Window>>,
	renderer: Option<WGPURenderer<'window>>
}

impl ApplicationHandler for App<'_> {
	fn resumed(&mut self, event_loop: &ActiveEventLoop) {
		if self.window.is_none() {
			let window_attrs = Window::default_attributes();
			let window = Arc::new(
				event_loop.create_window(window_attrs).
					expect("Failed to create window")
			);
			self.window = Some(window.clone());
			let renderer = pollster::block_on(WGPURenderer::new(window))
				.expect("Failed to create renderer");
			self.renderer = Some(renderer);
		}
		event_loop.set_control_flow(ControlFlow::Wait);
	}
	
	fn window_event(
		&mut self,
		event_loop: &ActiveEventLoop,
		_window_id: WindowId,
		event: WindowEvent
	) {
		match event {
			WindowEvent::CloseRequested => {
				event_loop.exit();
			}
			_ => {}
		}
	}
}

fn main() {
	let event_loop = EventLoop::new()
		.expect("Failed to create event loop");
	let mut app = App::default();
	event_loop.run_app(&mut app)
		.expect("Failed to start event loop");
}

Проблема в том, что я хочу добавить косвенность - я не хочу прибивать гвоздями App к WGPURenderer, я хочу сделать возможными в будущем написать разные реализации трейта Renderer - например, VulkanRenderer, OpenGLRenderer и т. д.

Но стоит мне сделать:

#[derive(Default)]
struct App<'window> {
	window: Option<Arc<Window>>,
	renderer: Option<Arc<dyn Renderer<'window>>> // Тут может быть и Box
}

...

let renderer = pollster::block_on(WGPURenderer::new(window))
	.expect("Failed to create renderer");
self.renderer = Some(Arc::new(renderer));

Я получаю:

error: lifetime may not live long enough
  --> src/main.rs:29:25
   |
17 |     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
   |                --------- has type `&mut App<'1>`
...
29 |             self.renderer = Some(Arc::new(renderer));
   |                                  ^^^^^^^^^^^^^^^^^^ coercion requires that `'1` must outlive `'static`

Как правильно организовать архитектуру приложения, чтобы можно было иметь общий код App (где расположен код обработки событий ввода и т. п.) на все бекэнды рендера с учётом того, что WGPU требует лайфтайм окна?

 ,

KivApple
()

Counter в Grafana

Есть приложение, в нём есть метрики-счётчики (counter), которые собираются в Prometheus, которые потом выводятся на графиках в Grafana.

Проблема в том, что рестарт приложения обнуляет счётчики.

Интересно, можно ли как-то с этим бороться, чтобы при выводе графиков учитывалось то что счётчики не могут уменьшаться, а могут только увеличиваться и, соответственно, их уменьшение игнорировалось и они продолжали расти дальше.

 ,

KivApple
()

Lorify RIP?

https://chromewebstore.google.com/detail/lorify/lcbahplohbljaoccfaionnkdgakdfomn

This extension is no longer available because it doesn't follow best practices for Chrome extensions.

Собственно, мой хром самовольно отключил расширение после обновления и не даёт включить обратно.

 

KivApple
()

Проверка доступности сайта из России

Есть ли какой-то сервис проверки, что сайт не подпадает под блокировки РКН ПО ФАКТУ?

Допустим, я администратор сайта, сам по себе я в список блокировок не попадаю, ничего осуждаемого с точки зрения РФ не делаю, но всегда есть риски, что IP раньше принадлежал кому-то, кто не нравился РКН или что кто-то, кто не нравился РКН направил одну из своих DNS записей на этот IP.

В этом случае, соответственно, надо попросить хостера IP поменять, если важна доступность сайта из РФ.

Но для этого об этом надо узнать. А узнать это проблематично, если сам в РФ не находишься. VPN в Россию, как я понимаю, совсем не обязательно применяют блокировки, потому что в датацентрах своя атмосфера.

Проверка списков ничего не гарантирует, так как если в список не включён явно судом именно мой домен или мой IP, то результат будет отрицательный.

Нужен сервис, который делает curl с домашнего провайдера в РФ на заданный домен и говорит вернулась ли заглушка или нет. В идеале, если он работает с набором провайдеров.

 ,

KivApple
()

VPN в Docker сеть

Есть хост с кучей Docker контейнеров.

Хочу запустить ещё один контейнер, в котором будет VPN сервер, дающий доступ к одной из сетей Docker. Причём таким образом, чтобы клиенты VPN могли подключаться к контейнерам подключенным к этой сети, а контейнеры в этой сети могли подключаться к клиентам VPN.

В Интернет же ходить клиентам не надо через VPN.

Как это лучше организовать? Ни сервер, ни клиенты не в России, так что устойчивость к РКН не нужна. Зато нужна беспроблемная работа VPN на Mac OS.

 ,

KivApple
()

Целесообразность перехода на более жирный сервер со старым CPU

Сейчас мои петы крутятся на VPS 2 ядра 2 гига озу 20 гб ssd. Дёшево, но постоянно надо думать о свободной памяти и диске, всё оптимизировать и иногда случаются инциденты а-ля Docker сожрал диск.

Есть вариант доплатить некоторую приемлемую сумму и перейти на bare metal server с 4 ядрами 32 гб ОЗУ 2 х 1 тб ssd.

Но есть нюанс. На сервере будет стоять Intel Xeon E3 1220, который вышел в 2011 году (сейчас VPS работает на AMD EPYC 7282).

Собственно, вопрос, не огребу ли я из-за этого проблем?

Из нагрузки nginx со статикой, postgres, бекэнды на разных языках устроенные по принципу «сходить в базу и отдать клиенту, упаковав результат в JSON, проверив пару условий», prometheus и grafana для мониторинга всего этого хозяйства. Всё крутится в докер контейнерах.

В данный момент производительность по CPU меня удовлетворяет, старый CPU в бенчмарках всего на 20% слабее на ядро, зато у меня будет в два раза больше ядер. С другой стороны, он может не умеет какие-то важные новые инструкции, а ещё сильнее просаживаться на заплатках от spectre и meltdown (хотя, возможно, их можно отключить, так как сервер исполняет только тот код, который я сам на него принёс, а user generated у меня только неисполняемые данные).

Ещё надо учитывать, что dedicated bare metal он полностью мой, а vps я с кем-то делю.

 ,

KivApple
()

Docker сожрал диск

Собственно, сабж

root@maumyrtille:~# du -shc /var/lib/docker/*
116K	/var/lib/docker/buildkit
5.4G	/var/lib/docker/containers
4.0K	/var/lib/docker/engine-id
27M	/var/lib/docker/image
236K	/var/lib/docker/network
6.9G	/var/lib/docker/overlay2
469M	/var/lib/docker/plugins
4.0K	/var/lib/docker/runtimes
4.0K	/var/lib/docker/swarm
8.0K	/var/lib/docker/tmp
554M	/var/lib/docker/volumes
14G	total
root@maumyrtille:~# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          22        21        2.842GB   236.8MB (8%)
Containers      25        24        1.212MB   0B (0%)
Local Volumes   19        16        564.3MB   0B (0%)
Build Cache     0         0         0B        0B
root@maumyrtille:~# docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - unused build cache

Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B

Чем занято более 10 ГБ?

 ,

KivApple
()

Что с Redis?

Я тут узнал, что Redis с версии 7.4 сменил лицензию, вроде как нарушил обратную совместимость формата БД и по этому поводу случилась драма и наплодились его форки. Например, KeyDB, Valkey и т. д.

Если присмотреться к форкам, то у них есть всякие интересные фичи. Например, обещают лучшую утилизацию многоядерных CPU. Или предоставляют возможность ставить EXPIRE на поля хешей или отдельных членов сетов, а не только ключи целиком.

Собственно, вопрос к лоравцам: кто чем пользуется и на что стоит посмотреть?

Перемещено leave из talks

 ,

KivApple
()

Один Docker контейнер не может связаться с другим Docker контейнером

Имеется два docker compose файла.

infra/docker-compose.yaml:

services:
  watchtower:
    container_name: watchtower
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $HOME/.docker/config.json:/config.json
    command: --label-enable
  prometheus:
    container_name: prometheus
    image: prom/prometheus
    user: root
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./data/prometheus:/prometheus
    networks:
      - prometheus-network
networks:
  prometheus-network:
    driver: bridge

app/docker-compose.yaml:

services:
  backend:
    container_name: app-backend
    image: ...
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
    networks:
      - postgres-network
      - prometheus-network
    depends_on:
      - postgres
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
  frontend:
    container_name: app-frontend
    ...
  postgres:
    container_name: app-postgres
    image: postgres:17-alpine
    ...
networks:
  postgres-network:
    driver: bridge
  prometheus-network:
    name: infra_prometheus-network
    external: true

Соответственно, запускается сначала infra, потом app.

Если зайти в контейнер app-backend и попробовать ping prometheus, то всё работает. Также можно попинговать самих себя - ping app-backend.

Если зайти в контейнер app-postgres и попробовать ping app-backend, то всё тоже работает.

Если зайти в контейнер prometheus и попробовать ping app-backend, то будет bad address 'app-backend'. Более того, если попробовать сделать ping prometheus, то эта команда тоже потерпит неудачу - то есть контейнер не видит сам себя.

Делаем docker inspect infra_prometheus-network:

[
    {
        "Name": "infra_prometheus-network",
        "Id": "ffec05671b873e60def53db831d4eb3966ba39bab74c01e8c7175362ae7da347",
        "Created": "2024-10-11T22:59:25.742311072Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.24.0.0/16",
                    "Gateway": "172.24.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "09bf47aa0f971ba2c9b686cb7bef947bd32eb7351ad41ec318118399345785d4": {
                "Name": "prometheus",
                "EndpointID": "dea3bb0a3e470cbcc9561ad8ef481c363f24a8c98628c4efd9d0193060092961",
                "MacAddress": "02:42:ac:18:00:02",
                "IPv4Address": "172.24.0.2/16",
                "IPv6Address": ""
            },
            "fa4dc2216105c90ccb1d165b6ce2b99dd43263a1bb459b43d0ce44a89330a9f8": {
                "Name": "app-backend",
                "EndpointID": "caa1b1371b290e7afd0273dd9a4e02781e2c895ed21d2913585739e233bc4784",
                "MacAddress": "02:42:ac:18:00:03",
                "IPv4Address": "172.24.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "prometheus-network",
            "com.docker.compose.project": "infra",
            "com.docker.compose.version": "2.29.7"
        }
    }
]

Видим, что IP контейнера app-backend - 172.24.0.3. Заходим в контейнер prometheus и делаем ping 172.24.0.3 - всё работает. То есть связь между контейнерами есть, но DNS не работает, причём только в одну сторону.

Пинговать внешние ресурсы типа Google из контейнера prometheus успешно получается, так что такой хотя бы DNS работает.

В чём может быть дело?

UPD: Проблема в официальном образе прометиуса - Один Docker контейнер не может связаться с другим Docker контейнером (комментарий), точнее скорее даже в базовом образе из которого он сделан. На неофициальном образе на базе alpine всё работает.

 ,

KivApple
()

Хранение в IndexedDB диапазона значений и выборка по одному значению

Мне нужно сохранить в IndexedDB в Веб-приложении в браузере набор событий. Каждое событие характеризуется датой начала и датой конца.

Затем мне нужно выбирать события актуальные в заданный день (например, сегодня).

В SQL это выглядело бы как-то так:

SELECT * FROM events WHERE startDate >= $1 AND endDate <= $1

Но в IndexedDB нельзя делать запросы в такой свободной форме.

Есть две гипотетические возможности:

Создать композитный индекс и использовать его, но они очень плохо документированны и непонятно можно ли как-то сделать так, чтобы для startDate использовался IDBKeyRange.lowerBound, а для endDate использовался IDBKeyRange.upperBound в одном и том же запросе.

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

 indexeddb, ,

KivApple
()

Токены авторизации в веб-приложениях

Вот чисто интуитивно есть два варианта реализации авторизации в веб-приложении:

1) Храним сессию в БД, юзеру выдаём после успешного ввода логина-пароля ключ (разумеется, ключ должен быть длинным и случайным, чтобы исключить угадывание). При каждом запросе вытаскиваем из БД сессию по ключу и проверяем, что сессия валидна.

2) Шифруем важные для нас данные об учётке и отдаем юзеру в качестве токена. При каждом запросе дешифруем токен и если получается осмысленный результат, то считаем юзера авторизованным, в БД не ходим. Так как ключ шифрования недоступен юзеру, то это тоже надёжно.

Второй вариант с точки зрения производительности выгоднее первого, так как мы уменьшаем I/O (плюс общую БД тяжелее масштабировать, чем инстансы приложения, так что жрать CPU лучше, чем жрать I/O).

Но возникает одно НО. На многих сайтах есть функция «выйти со всех устройств» или даже список сессий и возможность кикнуть любую сессию. Это полезно в случае, если нечаянно залогинился на чужом компьютере или если тебя поломали, но ты успел сменить пароль.

Как это реализовано в первом варианте понятно - простое удаление сессии из БД. Как это реализовывать во втором варианте - непонятно.

Возникает вопрос, как устроены крупные сервисы типа всяких гуглов. Если посмотреть запросы, которые шлёт сайт гугла, там будут JWT токены, которые предполагают второй вариант (но, конечно, не гарантируют). Но у гугла есть опция, например, при смене пароля выбить все сессии во всех браузерах. Значит ли это, что гугл на каждый запрос к своему API дёргает БД сессий и JWT там только для красоты или каких-нибудь неважных сервисов.

В голову пришёл ещё компромиссный вариант, что можно гонять короткоживущие токены второго типа, но с расчётом, что они будут постоянно протухать и будет происходить переавторизация по токену первого типа. В этом случае получается, что функция «выйти со всех устройств» работать будет, но с лагом несколько минут/часов.

Интересно как это сделано в крупных сервисах. Используется ли вариант один, два или гибридный. Как во втором варианте работает функция выхода со всех устройств. Какой срок жизни токенов используется в третьем варианте обычно.

 

KivApple
()

SDL3 + Vulkan на Mac OS X

Имеется Mac OS X, на которую установлен MoltenVk с помощью команды

brew install vulkan-tools

Этот пакет по зависимостям подтягивает и molten-vk, и vulkan-headers. Команда vulkaninfo успешно отрабатывает и показывает всякую информацию о Vulkan.

Затем, имеется простой код создающий окно в SDL (для простоты примера опущен код цикла событий, освобождения ресурсов и т. д.):

#include <stdio.h>
#include <SDL3/SDL.h>

int main(int argc, char *argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }
    SDL_Window *window = SDL_CreateWindow("My app", 800, 600, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_VULKAN);
    if (!window) {
        printf("Unable to create a window: %s", SDL_GetError());
        return 1;
    }
    return 0;
}

Код собирается следующим скриптом CMake:

cmake_minimum_required(VERSION 3.28)
project(MyApp)

set(BUILD_SHARED_LIBS OFF)

find_package(Vulkan REQUIRED)
add_subdirectory(lib/volk)
add_subdirectory(lib/SDL)

add_executable(MyApp main.c)
target_link_libraries(MyApp SDL3::SDL3-static volk::volk)

В данном случае lib/SDL это клонированный master https://github.com/libsdl-org/SDL, а lib/volk это клонированный master https://github.com/zeux/volk.

При запуске код выдаёт ошибку:

Unable to create a window: Failed to load Vulkan Portability library

В чём может быть проблема?

 ,

KivApple
()

RSS подписка на новые темы