ci / check (push) Failing after 6m27s
Brings in 0.4.8: connection log, uninstall and update from the awg-ctrl menu, secret panel path, stealth hub and sen:// port, random I1/MTU on install. install.sh keeps the new output (spinner, cards, outdated-install wipe) on top of the reorganised installer; an outdated install can be reinstalled with a wipe or removed. The connection log tab uses the MD3 segmented button; its metrics-token scrape snippet moved to Settings.
225 lines
7.8 KiB
Go
225 lines
7.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
|
|
"github.com/maeneko/forgetting/internal/agent"
|
|
"github.com/maeneko/forgetting/internal/jsjson"
|
|
)
|
|
|
|
// Хаб нод: WebSocket — только агенту с подписью в заголовках; остальным 404 пустого сервера.
|
|
// Старых агентов (без подписи) пускают, пока не все ноды подключались по-новому.
|
|
func TestHubPreauth(t *testing.T) {
|
|
p := newMovePanel(t, "on", "")
|
|
pub3, priv3, _ := ed25519.GenerateKey(rand.Reader)
|
|
_, stranger, _ := ed25519.GenerateKey(rand.Reader)
|
|
if _, err := p.a.db.Exec("INSERT INTO nodes (id, name, pub, created_at) VALUES (3, 'Frankfurt', ?, 1)", b64url.EncodeToString(pub3)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(p.a.nodes.serveWS))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
hdr := func(id int64, key ed25519.PrivateKey, ts int64) http.Header {
|
|
h := http.Header{}
|
|
h.Set("X-Node-Id", strconv.FormatInt(id, 10))
|
|
h.Set("X-Node-Ts", strconv.FormatInt(ts, 10))
|
|
h.Set("X-Node-Sig", b64url.EncodeToString(ed25519.Sign(key, preauthMsg(id, ts))))
|
|
return h
|
|
}
|
|
// dial — код ответа на апгрейд (101 — WebSocket принят) и первый кадр.
|
|
dial := func(h http.Header) (int, *websocket.Conn) {
|
|
ws, resp, err := websocket.Dial(ctx, wsURL, &websocket.DialOptions{HTTPHeader: h})
|
|
if err != nil {
|
|
if resp == nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode == 404 && string(body) != "404 page not found\n" {
|
|
t.Errorf("тело 404 не как у пустого сервера: %q", body)
|
|
}
|
|
return resp.StatusCode, nil
|
|
}
|
|
return 101, ws
|
|
}
|
|
read := func(ws *websocket.Conn) jsjson.Object {
|
|
_, raw, err := ws.Read(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
v, _ := jsjson.Decode(raw)
|
|
o, _ := v.(jsjson.Object)
|
|
return o
|
|
}
|
|
auth := func(ws *websocket.Conn, id int64, key ed25519.PrivateKey) jsjson.Object {
|
|
nonce := read(ws).Get2("nonce").(string)
|
|
ws.Write(ctx, websocket.MessageText, jsjson.MustMarshal(jsjson.Object{{Key: "t", Val: "auth"}, {Key: "node_id", Val: id},
|
|
{Key: "sig", Val: b64url.EncodeToString(ed25519.Sign(key, authMsg(nonce, id)))}, {Key: "version", Val: MinNodeVersion}}))
|
|
return read(ws)
|
|
}
|
|
now := time.Now().Unix()
|
|
|
|
if code, _ := dial(hdr(3, stranger, now)); code != 404 {
|
|
t.Errorf("чужая подпись: %d, ждали 404", code)
|
|
}
|
|
if code, _ := dial(hdr(99, stranger, now)); code != 404 {
|
|
t.Errorf("несуществующая нода: %d, ждали 404", code)
|
|
}
|
|
if code, _ := dial(hdr(3, priv3, now-2*preauthWindow)); code != 404 {
|
|
t.Errorf("подпись вне окна: %d, ждали 404", code)
|
|
}
|
|
|
|
// Переходный режим: нода 3 ещё не подключалась с подписью — старый агент проходит.
|
|
code, ws := dial(nil)
|
|
if code != 101 {
|
|
t.Fatalf("старый агент в переходном режиме: %d", code)
|
|
}
|
|
if r := auth(ws, 3, priv3); r.Get2("t") != "ready" {
|
|
t.Fatalf("старый агент: %v", r)
|
|
}
|
|
ws.CloseNow()
|
|
|
|
// Новый агент: подпись в заголовках, затем прежний auth внутри.
|
|
code, ws = dial(hdr(3, priv3, now))
|
|
if code != 101 {
|
|
t.Fatalf("новый агент: %d", code)
|
|
}
|
|
if r := auth(ws, 3, priv3); r.Get2("t") != "ready" {
|
|
t.Fatalf("новый агент: %v", r)
|
|
}
|
|
ws.CloseNow()
|
|
|
|
// Все ноды подключались с подписью — без неё больше не пускают.
|
|
if code, _ := dial(nil); code != 404 {
|
|
t.Errorf("без подписи после перехода: %d, ждали 404", code)
|
|
}
|
|
resp, err := http.Get(srv.URL + "/node/v1")
|
|
if err != nil || resp.StatusCode != 404 {
|
|
t.Errorf("обычный GET: %v %v", resp, err)
|
|
}
|
|
t.Setenv("HUB_LEGACY", "on")
|
|
if code, _ := dial(nil); code != 101 {
|
|
t.Errorf("HUB_LEGACY=on: %d", code)
|
|
}
|
|
t.Setenv("HUB_LEGACY", "")
|
|
|
|
// Подпись одной ноды, auth от имени другой — отказ.
|
|
pub5, priv5, _ := ed25519.GenerateKey(rand.Reader)
|
|
p.a.db.Exec("INSERT INTO nodes (id, name, pub, created_at, preauth) VALUES (5, 'Riga', ?, 1, 1)", b64url.EncodeToString(pub5))
|
|
code, ws = dial(hdr(3, priv3, now+1))
|
|
if code != 101 {
|
|
t.Fatalf("нода 3: %d", code)
|
|
}
|
|
if r := auth(ws, 5, priv5); r != nil {
|
|
t.Errorf("auth чужой ноды после подписи ноды 3 принят: %v", r)
|
|
}
|
|
ws.CloseNow()
|
|
}
|
|
|
|
// Настоящий агент привязывается к настоящему хабу по строке join и подключается снова уже
|
|
// своим ключом — в обоих случаях с подписью до апгрейда.
|
|
func TestHubAgentEndToEnd(t *testing.T) {
|
|
t.Setenv("HUB_LEGACY", "off")
|
|
p := newMovePanel(t, "on", "")
|
|
secretRaw := make([]byte, 16)
|
|
rand.Read(secretRaw)
|
|
secret := b64url.EncodeToString(secretRaw)
|
|
h := sha256.Sum256(secretRaw)
|
|
if _, err := p.a.db.Exec("INSERT INTO nodes (id, name, join_hash, join_expires, created_at) VALUES (7, 'Новая', ?, ?, 1)",
|
|
h[:], time.Now().Unix()+600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(p.a.nodes.serveWS))
|
|
defer srv.Close()
|
|
u, _ := url.Parse(srv.URL)
|
|
port, _ := strconv.Atoi(u.Port())
|
|
pin := certPin(t, srv.Certificate().Raw)
|
|
|
|
dir := t.TempDir()
|
|
_, ik, _ := ed25519.GenerateKey(rand.Reader)
|
|
der, _ := x509.MarshalPKCS8PrivateKey(ik)
|
|
os.WriteFile(filepath.Join(dir, "internal.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600)
|
|
env := filepath.Join(dir, "cli.env")
|
|
os.WriteFile(env, []byte("JOIN_SECRET="+secret+"\nNODE_ID=7\n"), 0o600)
|
|
run := func() {
|
|
a, err := agent.New(agent.Config{
|
|
CoreHost: u.Hostname(), CorePort: port, CorePin: pin, NodeID: 7, Ctrl: "http://127.0.0.1:1",
|
|
NodeKeyFile: filepath.Join(dir, "node.key"), InternalKey: filepath.Join(dir, "internal.key"),
|
|
CLIEnvFile: env, JoinSecret: envValue(env, "JOIN_SECRET"), Version: MinNodeVersion,
|
|
Logf: t.Logf, Errorf: t.Logf,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go a.Run(ctx)
|
|
for range 100 {
|
|
if p.a.nodes.IsOnline(7) {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Fatal("агент не подключился")
|
|
}
|
|
|
|
run() // привязка по секрету
|
|
row := p.a.nodes.row(7)
|
|
if !row.Pub.Valid {
|
|
t.Fatal("ключ ноды не сохранён после привязки")
|
|
}
|
|
var pre int
|
|
p.a.db.QueryRow("SELECT preauth FROM nodes WHERE id = 7").Scan(&pre)
|
|
if pre != 1 {
|
|
t.Error("подключение с подписью не отмечено (preauth)")
|
|
}
|
|
for range 100 { // прежнее соединение снимается при отмене контекста
|
|
if !p.a.nodes.IsOnline(7) {
|
|
break
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
if strings.Contains(envValue(env, "JOIN_SECRET"), secret) {
|
|
t.Error("секрет привязки не стёрт после первого подключения")
|
|
}
|
|
run() // повторное подключение — уже своим ключом
|
|
}
|
|
|
|
func certPin(t *testing.T, raw []byte) string {
|
|
cert, err := x509.ParseCertificate(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
|
|
return b64url.EncodeToString(sum[:])
|
|
}
|
|
|
|
func envValue(file, key string) string {
|
|
raw, _ := os.ReadFile(file)
|
|
for _, l := range strings.Split(string(raw), "\n") {
|
|
if v, ok := strings.CutPrefix(l, key+"="); ok {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|