Panel redesign, per-server I1 masking, node minimum version 0.5.0, installer output
ci / check (push) Successful in 23m54s
ci / check (push) Successful in 23m54s
- UI: settings tab with sections (server, API keys, Prometheus, audit, migrate, about); server settings moved there; master keys table rework (menu, chips, version, compact phone cards); three-state node version chip - I1: random per-server template (DNS / QUIC Initial / STUN) kept in users.db, GET/POST /api/masking, change from the master keys banner - Hub rejects nodes older than 0.5.0 (close 4026), /ui/nodes reports outdated - install.sh: spinner for long commands (full output in the log), step progress bar, cards; installs older than 0.5.0 require a full reinstall with data reset - Bump to Beta 0.5.0
This commit is contained in:
@@ -91,7 +91,7 @@ func TestPrometheusOnNodeZero(t *testing.T) {
|
||||
ag, err := agent.New(agent.Config{
|
||||
CoreHost: "127.0.0.1", CorePort: hubPort, CorePin: b64url.EncodeToString(a.nodes.pin), NodeID: 0,
|
||||
Ctrl: ctrl.URL, NodeKeyFile: keyFile, InternalKey: filepath.Join(dir, "none"),
|
||||
PromURL: prom.URL, MetricsPort: metricsPort, MonitorHash: hashFile,
|
||||
PromURL: prom.URL, MetricsPort: metricsPort, MonitorHash: hashFile, Version: MinNodeVersion,
|
||||
Logf: t.Logf, Errorf: t.Logf,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+39
-3
@@ -159,8 +159,11 @@ func newNodes(a *App) (*Nodes, error) {
|
||||
keyF, crtF := file("NODE_TLS_KEY_FILE", "node_tls.key"), file("NODE_TLS_CERT_FILE", "node_tls.crt")
|
||||
if err := func() error {
|
||||
if !exists(keyF) && !exists(crtF) {
|
||||
// CN случайный (12 hex, как имя хоста контейнера): постоянный CN — готовый отпечаток для DPI.
|
||||
cn := make([]byte, 6)
|
||||
rand.Read(cn)
|
||||
cmd := exec.Command("openssl", "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", "-nodes",
|
||||
"-keyout", keyF, "-out", crtF, "-days", "3650", "-subj", "/CN=awg-node")
|
||||
"-keyout", keyF, "-out", crtF, "-days", "3650", "-subj", fmt.Sprintf("/CN=%x", cn))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -473,6 +476,30 @@ func (n *Nodes) dropConn(id int64, c *nodeConn) {
|
||||
n.mu.Unlock()
|
||||
}
|
||||
|
||||
// MinNodeVersion — самая старая версия ноды, с которой работает панель (в UI — MIN_NODE_VERSION).
|
||||
const MinNodeVersion = "0.5.0"
|
||||
|
||||
// closeOutdated — код закрытия для ноды старше MinNodeVersion.
|
||||
const closeOutdated = 4026
|
||||
|
||||
// cmpVersion сравнивает версии вида 0.5.0 (хвост после «-» не учитывается): <0 — a старше b.
|
||||
func cmpVersion(a, b string) int {
|
||||
pa, pb := strings.Split(strings.SplitN(a, "-", 2)[0], "."), strings.Split(strings.SplitN(b, "-", 2)[0], ".")
|
||||
for i := 0; i < max(len(pa), len(pb)); i++ {
|
||||
var x, y int
|
||||
if i < len(pa) {
|
||||
x, _ = strconv.Atoi(pa[i])
|
||||
}
|
||||
if i < len(pb) {
|
||||
y, _ = strconv.Atoi(pb[i])
|
||||
}
|
||||
if x != y {
|
||||
return x - y
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func closeWS(ws *websocket.Conn, code int, reason string) {
|
||||
go ws.Close(websocket.StatusCode(code), reason)
|
||||
}
|
||||
@@ -540,6 +567,13 @@ func (n *Nodes) serveWS(w http.ResponseWriter, r *http.Request) {
|
||||
version = v
|
||||
}
|
||||
n.a.db.Exec("UPDATE nodes SET last_seen = ?, version = ? WHERE id = ?", now(), version, row.ID)
|
||||
// Нода старше MinNodeVersion (или без версии — агент старше 0.3.4) с этой панелью не работает:
|
||||
// соединение не принимаем, для панели она «нет связи», пока её не обновят (агент переподключится сам).
|
||||
if v, _ := version.(string); v == "" || cmpVersion(v, MinNodeVersion) < 0 {
|
||||
logf("nodes: «%s» (#%d) — версия %q, нужна не ниже %s: соединение отклонено", row.Name, row.ID, v, MinNodeVersion)
|
||||
closeWS(ws, closeOutdated, "outdated "+MinNodeVersion)
|
||||
return
|
||||
}
|
||||
|
||||
conn = &nodeConn{ws: ws, alive: true, pending: map[int64]*pending{}}
|
||||
nodeID = row.ID
|
||||
@@ -775,7 +809,8 @@ func (n *Nodes) List() []serverInfo {
|
||||
}
|
||||
for _, r := range n.all() {
|
||||
out = append(out, serverInfo{ID: int(r.ID), Name: r.Name, Online: online[r.ID], Pending: !r.Pub.Valid,
|
||||
LastSeen: nullInt(r.LastSeen), Version: nullStr(r.Version), Health: hs[r.ID]})
|
||||
LastSeen: nullInt(r.LastSeen), Version: nullStr(r.Version), Health: hs[r.ID],
|
||||
Outdated: r.Pub.Valid && (!r.Version.Valid || cmpVersion(r.Version.String, MinNodeVersion) < 0)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -789,11 +824,12 @@ type serverInfo struct {
|
||||
LastSeen any
|
||||
Version any
|
||||
Health *health
|
||||
Outdated bool // старше MinNodeVersion: хаб её не подключает, пока не обновят
|
||||
}
|
||||
|
||||
func (s serverInfo) json() jsjson.Object {
|
||||
return jsjson.Object{{Key: "id", Val: s.ID}, {Key: "name", Val: s.Name}, {Key: "local", Val: s.Local}, {Key: "online", Val: s.Online},
|
||||
{Key: "pending", Val: s.Pending}, {Key: "last_seen", Val: s.LastSeen}, {Key: "version", Val: s.Version}, {Key: "health", Val: s.Health.json()}}
|
||||
{Key: "pending", Val: s.Pending}, {Key: "last_seen", Val: s.LastSeen}, {Key: "version", Val: s.Version}, {Key: "health", Val: s.Health.json()}, {Key: "outdated", Val: s.Outdated}}
|
||||
}
|
||||
|
||||
func (n *Nodes) idOf(c *Ctx) (int64, bool) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"github.com/maeneko/forgetting/internal/jsjson"
|
||||
)
|
||||
|
||||
// Нода старше MinNodeVersion (или без версии) с панелью не работает: хаб отклоняет её с 4026 и
|
||||
// не считает подключённой, но версию запоминает — панель показывает, до чего обновлять.
|
||||
func TestOutdatedNodeRejected(t *testing.T) {
|
||||
p := newMovePanel(t, "off", "40500")
|
||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
if _, err := p.a.db.Exec("INSERT INTO nodes (id, name, pub, created_at) VALUES (3, 'Frankfurt', ?, 1)", b64url.EncodeToString(pub)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(p.a.nodes.serveWS))
|
||||
defer srv.Close()
|
||||
|
||||
connect := func(version string) (jsjson.Object, websocket.StatusCode) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ws, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(srv.URL, "http"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ws.CloseNow()
|
||||
read := func() (jsjson.Object, error) {
|
||||
_, raw, err := ws.Read(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, _ := jsjson.Decode(raw)
|
||||
o, _ := v.(jsjson.Object)
|
||||
return o, nil
|
||||
}
|
||||
hello, _ := read()
|
||||
auth := jsjson.Object{{Key: "t", Val: "auth"}, {Key: "node_id", Val: 3},
|
||||
{Key: "sig", Val: b64url.EncodeToString(ed25519.Sign(priv, authMsg(hello.Get2("nonce").(string), 3)))}}
|
||||
if version != "" {
|
||||
auth = append(auth, jsjson.KV{Key: "version", Val: version})
|
||||
}
|
||||
ws.Write(ctx, websocket.MessageText, jsjson.MustMarshal(auth))
|
||||
o, err := read()
|
||||
return o, websocket.CloseStatus(err)
|
||||
}
|
||||
|
||||
for _, v := range []string{"0.4.9", ""} {
|
||||
if o, code := connect(v); code != closeOutdated {
|
||||
t.Fatalf("версия %q: ждали закрытия %d, получили %v / %v", v, closeOutdated, o, code)
|
||||
}
|
||||
p.a.nodes.mu.Lock()
|
||||
_, on := p.a.nodes.conns[3]
|
||||
p.a.nodes.mu.Unlock()
|
||||
if on {
|
||||
t.Fatalf("версия %q: нода считается подключённой", v)
|
||||
}
|
||||
}
|
||||
var stored string
|
||||
p.a.db.QueryRow("SELECT COALESCE(version, '') FROM nodes WHERE id = 3").Scan(&stored)
|
||||
if stored != "" { // последняя попытка — без версии
|
||||
t.Fatalf("версия в БД: %q", stored)
|
||||
}
|
||||
if o, code := connect(MinNodeVersion); o.Get2("t") != "ready" {
|
||||
t.Fatalf("актуальная версия: %v / %v", o, code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmpVersion(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
a, b string
|
||||
sign int
|
||||
}{{"0.5.0", "0.5.0", 0}, {"0.4.9", "0.5.0", -1}, {"0.10.0", "0.9.9", 1}, {"0.5", "0.5.0", 0}, {"0.5.1-beta", "0.5.1", 0}} {
|
||||
got := cmpVersion(c.a, c.b)
|
||||
if (got > 0) != (c.sign > 0) || (got < 0) != (c.sign < 0) {
|
||||
t.Errorf("cmpVersion(%q, %q) = %d", c.a, c.b, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestRetireReadiness(t *testing.T) {
|
||||
write := func(o jsjson.Object) { ws.Write(ctx, websocket.MessageText, jsjson.MustMarshal(o)) }
|
||||
nonce := read().Get2("nonce").(string)
|
||||
write(jsjson.Object{{Key: "t", Val: "auth"}, {Key: "node_id", Val: 3},
|
||||
{Key: "sig", Val: b64url.EncodeToString(ed25519.Sign(priv, authMsg(nonce, 3)))}})
|
||||
{Key: "sig", Val: b64url.EncodeToString(ed25519.Sign(priv, authMsg(nonce, 3)))}, {Key: "version", Val: MinNodeVersion}})
|
||||
if got := string(jsjson.MustMarshal(read())); got != `{"t":"ready","core":"198.51.100.20:40500"}` {
|
||||
t.Fatalf("ready: %s", got)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user