Compare commits
2
Commits
v0.4.2
...
ea91265674
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea91265674 | ||
|
|
de8a32ac41 |
@@ -11,4 +11,4 @@
|
|||||||
|
|
||||||
BRAND=Forgetting
|
BRAND=Forgetting
|
||||||
CHANNEL=Beta
|
CHANNEL=Beta
|
||||||
VERSION=0.4.2
|
VERSION=0.4.2.1
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ TOKEN=$(curl -s -X POST http://HOST:PORT/login \
|
|||||||
|
|
||||||
| Метод | Ответ |
|
| Метод | Ответ |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.2" }` — **без авторизации**, панель берёт отсюда вордмарк на экране логина |
|
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.4.2.1" }` — **без авторизации**, панель берёт отсюда вордмарк на экране логина |
|
||||||
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
|
||||||
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-agent",
|
"name": "awg-agent",
|
||||||
"version": "0.4.2",
|
"version": "0.4.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl",
|
"description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-control",
|
"name": "awg-control",
|
||||||
"version": "0.4.2",
|
"version": "0.4.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-ui",
|
"name": "awg-ui",
|
||||||
"version": "0.4.2",
|
"version": "0.4.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -149,6 +149,24 @@ export default function App() {
|
|||||||
if (touchStartX.current - touchCurrentX.current > 40) setDrawerOpen(false);
|
if (touchStartX.current - touchCurrentX.current > 40) setDrawerOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Токен протух (24 ч) или отозван, пока панель открыта: запросы вкладок падали с 401, а экран
|
||||||
|
// оставался прежним. Выходим только по 401 самой панели («Unauthorized» — её JWT не принят):
|
||||||
|
// 401 от awg-ctrl ноды (внутренняя авторизация) пересылается как есть и сессию не рвёт.
|
||||||
|
useEffect(() => {
|
||||||
|
const id = axios.interceptors.response.use(undefined, e => {
|
||||||
|
if (axios.isAxiosError(e) && e.response?.status === 401 && e.response.data?.error === 'Unauthorized') {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
setToken('');
|
||||||
|
setNodes([]);
|
||||||
|
setServerId(null);
|
||||||
|
setServerIdState(null);
|
||||||
|
showMsg('Сессия истекла — войдите снова');
|
||||||
|
}
|
||||||
|
return Promise.reject(e);
|
||||||
|
});
|
||||||
|
return () => axios.interceptors.response.eject(id);
|
||||||
|
}, [showMsg]);
|
||||||
|
|
||||||
// Восстановить сессию из сохранённого токена
|
// Восстановить сессию из сохранённого токена
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem(TOKEN_KEY);
|
const saved = localStorage.getItem(TOKEN_KEY);
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "awg-cli",
|
"name": "awg-cli",
|
||||||
"version": "0.4.2",
|
"version": "0.4.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ trap 'exec >&- 2>&-; wait "$LOG_PID" 2>/dev/null || true' EXIT
|
|||||||
# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает
|
# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает
|
||||||
# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может.
|
# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может.
|
||||||
# При бампе версии правь оба места.
|
# При бампе версии правь оба места.
|
||||||
VERSION="0.4.2"
|
VERSION="0.4.2.1"
|
||||||
CHANNEL="${CHANNEL:-Beta}"
|
CHANNEL="${CHANNEL:-Beta}"
|
||||||
|
|
||||||
# Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник
|
# Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ func (s *Server) collectMetrics() (*metricsBody, error) {
|
|||||||
if st.Up {
|
if st.Up {
|
||||||
stats = s.getPeersData()
|
stats = s.getPeersData()
|
||||||
}
|
}
|
||||||
|
torrent := s.torrent.SeenSet() // до запроса: внутри цикла по строкам — ни базы, ни nft
|
||||||
rows, err := s.db.Query("SELECT name, ip, pub_key FROM users WHERE vpn_key != ''")
|
rows, err := s.db.Query("SELECT name, ip, pub_key FROM users WHERE vpn_key != ''")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -141,7 +142,7 @@ func (s *Server) collectMetrics() (*metricsBody, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ps := stats[pub]
|
ps := stats[pub]
|
||||||
b.List = append(b.List, metricsPeer{"user", n, ip, ps.Online, orZero(ps.Rx), orZero(ps.Tx), s.shaping.PriorityOf(ip), s.torrent.Seen(ip)})
|
b.List = append(b.List, metricsPeer{"user", n, ip, ps.Online, orZero(ps.Rx), orZero(ps.Tx), s.shaping.PriorityOf(ip), torrent[ip]})
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
devs, err := s.peersAll()
|
devs, err := s.peersAll()
|
||||||
@@ -150,7 +151,7 @@ func (s *Server) collectMetrics() (*metricsBody, error) {
|
|||||||
}
|
}
|
||||||
for _, p := range devs {
|
for _, p := range devs {
|
||||||
ps := stats[p.PubKey]
|
ps := stats[p.PubKey]
|
||||||
b.List = append(b.List, metricsPeer{"device", "", p.IP, ps.Online, orZero(ps.Rx), orZero(ps.Tx), s.shaping.PriorityOf(p.IP), s.torrent.Seen(p.IP)})
|
b.List = append(b.List, metricsPeer{"device", "", p.IP, ps.Online, orZero(ps.Rx), orZero(ps.Tx), s.shaping.PriorityOf(p.IP), torrent[p.IP]})
|
||||||
}
|
}
|
||||||
b.Peers.Total = st.Peers
|
b.Peers.Total = st.Peers
|
||||||
for _, ps := range stats {
|
for _, ps := range stats {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package ctrl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maeneko/forgetting/internal/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLite открыт с одним соединением: любой запрос в базу изнутри цикла по строкам users ждал
|
||||||
|
// бы его вечно и намертво вешал awg-ctrl (так было в 0.4.2 с меткой торрента). Скрейп метрик
|
||||||
|
// идёт раз в 15 с, поэтому зависание проявлялось сразу.
|
||||||
|
func TestCollectMetricsNoDeadlock(t *testing.T) {
|
||||||
|
db, err := openDB(filepath.Join(t.TempDir(), "users.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
db.Exec("INSERT INTO users (name, ip, pub_key, vpn_key) VALUES (?, ?, ?, 'vpn://x')", fmt.Sprintf("u%d", i), fmt.Sprintf("10.9.0.%d", i+2), fmt.Sprintf("pk%d", i))
|
||||||
|
}
|
||||||
|
s := &Server{db: db, iface: "awg1", log: logx.New(os.Stderr, t.TempDir())}
|
||||||
|
s.shaping = newShaping(s)
|
||||||
|
s.torrent = newTorrent(s)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { _, err := s.collectMetrics(); done <- err }()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("collectMetrics завис: запрос в базу изнутри цикла по строкам")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ func (s *Server) routes() []route {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
R("GET", "/api/users/stats", true, a, func(c *rctx) (result, error) {
|
R("GET", "/api/users/stats", true, a, func(c *rctx) (result, error) {
|
||||||
|
torrent := s.torrent.SeenSet()
|
||||||
rows, err := s.db.Query("SELECT name, ip, pub_key, key_gen FROM users WHERE vpn_key != ''")
|
rows, err := s.db.Query("SELECT name, ip, pub_key, key_gen FROM users WHERE vpn_key != ''")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result{}, err
|
return result{}, err
|
||||||
@@ -88,7 +89,7 @@ func (s *Server) routes() []route {
|
|||||||
ps := stats[pub]
|
ps := stats[pub]
|
||||||
list = append(list, ob{{"name", n}, {"ip", ip}, {"key_gen", kg}, {"online", ps.Online},
|
list = append(list, ob{{"name", n}, {"ip", ip}, {"key_gen", kg}, {"online", ps.Online},
|
||||||
{"lastHandshake", ps.LastHandshake}, {"rx", ps.Rx}, {"tx", ps.Tx}, {"priority", s.shaping.PriorityOf(ip)},
|
{"lastHandshake", ps.LastHandshake}, {"rx", ps.Rx}, {"tx", ps.Tx}, {"priority", s.shaping.PriorityOf(ip)},
|
||||||
{"torrent", s.torrent.Seen(ip)}})
|
{"torrent", torrent[ip]}})
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
return ok(ob{{"users", list}})
|
return ok(ob{{"users", list}})
|
||||||
@@ -138,6 +139,7 @@ func (s *Server) routes() []route {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
R("GET", "/api/peers", true, a, func(c *rctx) (result, error) {
|
R("GET", "/api/peers", true, a, func(c *rctx) (result, error) {
|
||||||
|
torrent := s.torrent.SeenSet()
|
||||||
stats := s.getPeersData()
|
stats := s.getPeersData()
|
||||||
peers, err := s.peersAll()
|
peers, err := s.peersAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +150,7 @@ func (s *Server) routes() []route {
|
|||||||
ps := stats[p.PubKey]
|
ps := stats[p.PubKey]
|
||||||
list = append(list, ob{{"pub_key", p.PubKey}, {"ip", p.IP}, {"psk_key", p.PskKey}, {"owner", p.Owner},
|
list = append(list, ob{{"pub_key", p.PubKey}, {"ip", p.IP}, {"psk_key", p.PskKey}, {"owner", p.Owner},
|
||||||
{"online", ps.Online}, {"lastHandshake", ps.LastHandshake}, {"rx", ps.Rx}, {"tx", ps.Tx},
|
{"online", ps.Online}, {"lastHandshake", ps.LastHandshake}, {"rx", ps.Rx}, {"tx", ps.Tx},
|
||||||
{"priority", s.shaping.PriorityOf(p.IP)}, {"torrent", s.torrent.Seen(p.IP)}})
|
{"priority", s.shaping.PriorityOf(p.IP)}, {"torrent", torrent[p.IP]}})
|
||||||
}
|
}
|
||||||
return ok(ob{{"peers", list}})
|
return ok(ob{{"peers", list}})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ type Torrent struct {
|
|||||||
opMu sync.Mutex // сериализует nft
|
opMu sync.Mutex // сериализует nft
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
enabled bool // копия настройки в памяти: cached() зовут внутри циклов по строкам users, а
|
||||||
|
// соединение с SQLite одно — запрос в базу оттуда ждал бы его вечно
|
||||||
tcp bool
|
tcp bool
|
||||||
lastError string
|
lastError string
|
||||||
cacheAt time.Time
|
cacheAt time.Time
|
||||||
@@ -79,7 +81,11 @@ type Torrent struct {
|
|||||||
cacheHits float64
|
cacheHits float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTorrent(s *Server) *Torrent { return &Torrent{s: s} }
|
func newTorrent(s *Server) *Torrent {
|
||||||
|
t := &Torrent{s: s}
|
||||||
|
t.enabled = t.Settings().Enabled
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Torrent) Settings() TorrentSettings {
|
func (t *Torrent) Settings() TorrentSettings {
|
||||||
return TorrentSettings{Enabled: t.s.getCfg("torrentBlock", "0") == "1"}
|
return TorrentSettings{Enabled: t.s.getCfg("torrentBlock", "0") == "1"}
|
||||||
@@ -163,6 +169,9 @@ func (t *Torrent) Update(st TorrentSettings) error {
|
|||||||
v = "1"
|
v = "1"
|
||||||
}
|
}
|
||||||
t.s.setCfg("torrentBlock", v)
|
t.s.setCfg("torrentBlock", v)
|
||||||
|
t.mu.Lock()
|
||||||
|
t.enabled, t.cache = st.Enabled, nil
|
||||||
|
t.mu.Unlock()
|
||||||
t.opMu.Lock()
|
t.opMu.Lock()
|
||||||
err := t.applyNow()
|
err := t.applyNow()
|
||||||
t.opMu.Unlock()
|
t.opMu.Unlock()
|
||||||
@@ -228,11 +237,13 @@ func (t *Torrent) read() (seen map[string]bool, hits float64, ok bool) {
|
|||||||
return seen, hits, true
|
return seen, hits, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cached не трогает базу (только память и nft) — его можно звать где угодно.
|
||||||
func (t *Torrent) cached() (map[string]bool, float64, bool) {
|
func (t *Torrent) cached() (map[string]bool, float64, bool) {
|
||||||
if !t.Settings().Enabled {
|
t.mu.Lock()
|
||||||
|
if !t.enabled {
|
||||||
|
t.mu.Unlock()
|
||||||
return nil, 0, false
|
return nil, 0, false
|
||||||
}
|
}
|
||||||
t.mu.Lock()
|
|
||||||
if t.cache != nil && time.Since(t.cacheAt) < torrentCacheS {
|
if t.cache != nil && time.Since(t.cacheAt) < torrentCacheS {
|
||||||
c, h := t.cache, t.cacheHits
|
c, h := t.cache, t.cacheHits
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
@@ -249,10 +260,15 @@ func (t *Torrent) cached() (map[string]bool, float64, bool) {
|
|||||||
return seen, hits, true
|
return seen, hits, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seen — пытался ли этот IP открыть торрент-соединение за последние torrentSeenTTL.
|
// SeenSet — кто пытался открыть торрент-соединение за последние torrentSeenTTL. Берите его до
|
||||||
func (t *Torrent) Seen(ip string) bool {
|
// запроса к базе, а не внутри цикла по строкам: так на открытом курсоре не висит вызов nft.
|
||||||
|
func (t *Torrent) SeenSet() map[string]bool {
|
||||||
seen, _, _ := t.cached()
|
seen, _, _ := t.cached()
|
||||||
return seen[ip]
|
out := make(map[string]bool, len(seen))
|
||||||
|
for ip := range seen {
|
||||||
|
out[ip] = true
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeenList — IP с меткой «торренты», по порядку; шейпинг держит их в фоновом классе.
|
// SeenList — IP с меткой «торренты», по порядку; шейпинг держит их в фоновом классе.
|
||||||
|
|||||||
Reference in New Issue
Block a user