168 lines
9.5 KiB
Markdown
168 lines
9.5 KiB
Markdown
# 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](https://github.com/postfixadmin/postfixadmin) —
|
||
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);
|
||
- `setclientchannelgroup` dla wskazanego `client_database_id`;
|
||
- `clientmove` wskazanego `clid` do 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](#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
|
||
|
||
1. Skopiuj `.env.example` do `.env` i ustaw sekrety oraz dozwoloną domenę iframe.
|
||
2. Zachowaj `data/linuxiarz.rrd` (kopia dotychczasowej bazy z archiwum) albo
|
||
zainicjuj nową bazę — patrz sekcja [RRD](#rrd).
|
||
3. Zapewnij zapis do katalogu `data` dla UID 10001: `chown -R 10001:10001 data`.
|
||
4. Uruchom: `docker compose up -d --build`.
|
||
5. 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:
|
||
|
||
```html
|
||
<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 `<canvas>`, 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/http` nie dodaje nagłówka `Server`;
|
||
- 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 `-trimpath` i 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.
|