9.5 KiB
TS3 monolith (Go)
Jedna aplikacja zastępująca stare sk.linuxiarz.pl i ts3stats.linuxiarz.pl.
Wygląd dashboardu (topbar + sidebar, karty, panele, jasny/ciemny motyw)
jest celowo spójny ze stylem PostfixAdminRS —
ten sam układ tokenów kolorów, te same komponenty UI.
Funkcjonalność
- wykrycie klienta TeamSpeak po IP widzianym przez HTTP i ServerQuery;
- utworzenie permanentnego kanału z nazwą, tematem, opcjonalnym hasłem,
limitem użytkowników (
channel_maxclients) i jakością kodeka (channel_codec_quality, 1–10); setclientchannelgroupdla wskazanegoclient_database_id;clientmovewskazanegocliddo nowego kanału;- dashboard statystyk: liczba online na żywo (auto-odświeżanie co 60 s przez
GET /stats/live), uptime/wersja/platforma serwera (serverinfo), wykresy RRD: 12h, 24h, 72h, tydzień, miesiąc, rok — dane pobierane jako JSON i rysowane responsywnie w<canvas>po stronie przeglądarki (patrz sekcja RRD), z podglądem na powiększenie; - przełącznik jasny/ciemny motyw (zapamiętywany w
localStorage); - formularz pod
/embed/channel, przeznaczony do osadzenia na innej domenie.
Przy jednym kliencie pod danym IP wszystko działa automatycznie. Jeżeli kilka klientów TS3 ma ten sam publiczny IP (NAT), aplikacja prosi o wybór nicku i wysyła 6-cyfrowy kod prywatną wiadomością TS3. Dzięki temu nie przenosi przypadkowej osoby i nie nadaje jej Channel Admina.
Szybki development
cp .env.example .env # uzupełnij sekrety
make quick # najszybszy build (bez testów/trimpath) -> bin/ts3monolith
make dev # quick build + start z lokalnym .env
make test # go test ./...
make build # pełny build produkcyjny (testy + trimpath + bez symboli)
Uruchomienie — Docker
- Skopiuj
.env.exampledo.envi ustaw sekrety oraz dozwoloną domenę iframe. - Zachowaj
data/linuxiarz.rrd(kopia dotychczasowej bazy z archiwum) albo zainicjuj nową bazę — patrz sekcja RRD. - Zapewnij zapis do katalogu
datadla UID 10001:chown -R 10001:10001 data. - Uruchom:
docker compose up -d --build. - Reverse proxy kieruje HTTPS do
127.0.0.1:8080.
Uruchomienie — pakiet .deb + systemd
Alternatywa dla Dockera, zalecana na dedykowanym hoście Debian/Ubuntu:
make deb # scripts/build-deb.sh -> packages/ts3monolith_<wersja>_amd64.deb
sudo dpkg -i packages/ts3monolith_*.deb
sudo nano /etc/default/ts3monolith # APP_SECRET, TS_QUERY_USER, TS_QUERY_PASSWORD, ...
sudo -u ts3monolith /usr/lib/ts3monolith/init-rrd # tylko przy świeżej instalacji, bez starej bazy
sudo systemctl enable --now ts3monolith
scripts/build-deb.sh buduje wszystko w kontenerze Docker — dokładnie tak
samo jak odpowiadający skrypt w PostfixAdminRS — więc host nie potrzebuje
lokalnie zainstalowanego Go ani żadnych zależności poza samym Dockerem.
Wersja obrazu (golang:<wersja z go.mod>-bookworm) jest odczytywana z
go.mod, a sam moduł nie ma żadnych zewnętrznych zależności, więc build jest
w pełni offline po ściągnięciu obrazu. Wynikowy pakiet trafia do packages/.
Pakiet instaluje:
/usr/bin/ts3monolith— binarka;/etc/default/ts3monolith— zmienne środowiskowe (conffile);/lib/systemd/system/ts3monolith.service— jednostka z pełnym hardeningiem (ProtectSystem=strict,NoNewPrivileges,RestrictAddressFamilies,MemoryDenyWriteExecute,SystemCallFilter=@system-service, ...), takim samym duchem jak w PostfixAdminRS;/usr/lib/ts3monolith/init-rrd— jednorazowy skrypt tworzący świeżą bazę RRD z RRA pokrywającymi wszystkie zakresy wykresów (12h…rok), gdy nie ma archiwalnego pliku do skopiowania;- dedykowanego użytkownika systemowego
ts3monolith(bez loginu, bez home).
install.sh z korzenia repo zostaje jako prostszy, ręczny installer bez
budowania pakietu .deb (np. do szybkiego postawienia na hoście bez
uprawnień do dpkg).
Przykład iframe:
<iframe
src="https://app.example.org/embed/channel"
title="Utwórz kanał TeamSpeak"
loading="lazy"
referrerpolicy="no-referrer"
style="width:100%;min-height:620px;border:0">
</iframe>
FRAME_ANCESTORS musi zawierać origin strony nadrzędnej, np. https://www.example.org.
TeamSpeak ServerQuery
Najbezpieczniej uruchamiać aplikację na tym samym hoście lub w prywatnej sieci z TS3 i ustawić TS_QUERY_HOST=127.0.0.1/adres prywatny. Nie wystawiaj portu 10011 publicznie. Query powinien mieć tylko prawa potrzebne do: clientlist -ip -uid, channelcreate, setclientchannelgroup, clientmove, sendtextmessage, channeldelete (rollback), serverinfo (karty uptime/wersji na dashboardzie).
RRD
Aplikacja używa systemowego rrdtool, a nie biblioteki Go, więc istniejący plik RRD pozostaje bez migracji. Co 60 s liczona jest liczba klientów client_type=0 na wybranym virtual serverze i zapisywana do DS online. Błąd ServerQuery nie zapisuje sztucznego zera.
Świeża instalacja bez archiwalnej bazy: packaging/ts3monolith-init-rrd
(instalowany jako /usr/lib/ts3monolith/init-rrd w pakiecie .deb) tworzy
nową bazę rrdtool create z RRA pokrywającymi 12h/24h/72h/tydzień/miesiąc/rok
przy kroku 60 s — ten sam layout logiczny co istniejące wykresy aplikacji.
Wykresy na dashboardzie nie są PNG renderowanymi przez rrdtool graph.
GET /stats/graph/{zakres} (internal/rrd/rrd.go, Manager.Fetch) woła
rrdtool fetch i zwraca surowe próbki jako mały JSON ({"points":[{"t":..., "v":...},...],"current":...,"average":...,"max":...}); front-end
(internal/web/assets/app.js) rysuje z tego wykres samodzielnie w
<canvas>, kolorami wziętymi na żywo z CSS (--line/--muted/--accent), więc przy przełączeniu jasny/ciemny motyw wykres się po prostu przerysowuje z już pobranych danych — bez kolejnego zapytania do serwera i bez osobnych plików PNG per motyw. Dzięki temu wykres jest też w pełni responsywny (łącznie z telefonem) i powiększenie w lightboksie to kolejne przerysowanie tych samych danych w większym `, a nie osobny plik.
Fetch dobiera funkcję konsolidacji RRD do zakresu: LAST dla 12h/24h/72h
(pełna rozdzielczość, RRA RRA:LAST:0.5:1:4500), AVERAGE dla
tydzień/miesiąc/rok (te okna istnieją tylko w gęściej uśrednionych RRA —
pytanie ich o LAST nie zwróciłoby nic starszego niż ok. 75h).
Jeżeli docelowy serwer ma inną architekturę/endianness niż źródłowy x86_64 little-endian, przenieś RRD przez rrdtool dump/rrdtool restore zamiast kopiowania binarnego pliku.
Bezpieczeństwo / fingerprinting
- brak PHP, frameworków frontendowych, CDN i nagłówka
X-Powered-By; - standardowy
net/httpnie dodaje nagłówkaServer; - CSP z kontrolowanym
frame-ancestors, HSTS,nosniff, restrykcyjne Permissions-Policy,style-src/script-src 'self'(bez inline — cały CSS/JS w plikach statycznych); - nagłówki proxy są ufane wyłącznie z
TRUSTED_PROXIES; - stateless HMAC tokeny formularzy i klientów, sprawdzanie
Origin, limity request body i rate limiting; - sekrety tylko przez środowisko, brak danych dostępowych w repo;
- rollback kanału, jeśli nadanie grupy lub przeniesienie klienta się nie powiedzie;
- kontener non-root,
cap_drop: ALL,no-new-privileges, read-only root filesystem (Docker) albo analogiczny hardening jednostki systemd (pakiet.deb); - build z
-trimpathi usuniętymi symbolami/debug info.
Nie da się zagwarantować, że stos technologiczny będzie całkowicie nierozpoznawalny na podstawie zachowania sieci/TLS. Celem jest brak prostych bannerów, wersji i sygnatur frameworka.
Rate limiting
Domyślnie wyłączony (RATE_LIMIT_ENABLED=false) — endpointy mają limity
zahardkodowane w kodzie od początku, ale same w sobie nie chronią niczego
krytycznego (ServerQuery i tak ma własny timeout, channelcreate i tak
wymaga poprawnego tokenu CSRF), a przy nietypowym ruchu (kilku odwiedzających
za tym samym IP/NAT, kilka kart, lightbox z wykresami) dawały false-positive
429 bez realnej korzyści. Włącz (RATE_LIMIT_ENABLED=true), jeśli chcesz
dodatkową warstwę ochrony przed nadużyciami na konkretnym endpoincie.
Każdy limit jest per-IP (po rozwiązaniu TRUSTED_PROXIES/X-Forwarded-For,
patrz clientIP() w internal/web/server.go) i liczony w oknie czasowym
podanym w nawiasie:
| Zmienna | Endpoint | Domyślnie | Co chroni |
|---|---|---|---|
RATE_LIMIT_STATS |
GET /stats (na minutę) |
60 | render strony statystyk |
RATE_LIMIT_STATS_LIVE |
GET /stats/live (na minutę) |
60 | auto-odświeżanie licznika online co 60 s |
RATE_LIMIT_GRAPH |
GET /stats/graph/{zakres} (na minutę) |
600 | dane wykresów — każde trafienie to jedno tanie rrdtool fetch, limit jest wysoki, bo jeden widok strony sam zużywa 6 zakresów, a NAT/kilka kart to łatwo pomnoży |
RATE_LIMIT_CHANNEL |
POST /channel/create (na minutę) |
40 | tworzenie kanału |
RATE_LIMIT_VERIFY |
POST /verify/start (na minutę) |
10 | start weryfikacji nicku (NAT) |
RATE_LIMIT_VERIFY_DONE |
POST /verify/complete (na minutę) |
10 | wpisanie 6-cyfrowego kodu |
RATE_LIMIT_CREATE |
POST /channel/create po weryfikacji (na godzinę) |
5 | utworzenie kanału po weryfikacji nicku przez NAT |
Przy przekroczeniu limitu endpoint odpowiada 429 Too Many Requests z
nagłówkiem Retry-After: 60. Wartości domyślne są w .env.example i
packaging/ts3monolith.default — dostosuj je do realnego ruchu na danej
instalacji zanim włączysz RATE_LIMIT_ENABLED na produkcji.