ci / check (push) Successful in 6m6s
- internal/jsjson: JSON.stringify-compatible encoder and order-preserving decoder, tested against fixtures produced by Node itself - internal/ctrl: awg-ctrl in Go (params, vpn:// keys, users, peers, sync, 3.1 upgrade, CAKE shaping, metrics, monitor job, Express-compatible HTTP layer) - internal/iauth, internal/logx: internal Ed25519 tokens and logging - tools/diffbench: Docker bench with stubbed awg/ip/tc/nft that runs one scenario against the TS and Go implementations for comparison Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
234 lines
6.2 KiB
Go
234 lines
6.2 KiB
Go
package ctrl
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Схема users.db — та же, что у Node-версии: существующие базы открываются как есть.
|
|
// PRAGMA не задаём (журнал по умолчанию, без WAL); ждём блокировку 5 с, как better-sqlite3.
|
|
|
|
type userRow struct {
|
|
Name string `json:"name"`
|
|
IP string `json:"ip"`
|
|
PubKey string `json:"pub_key"`
|
|
VpnKey string `json:"vpn_key"`
|
|
PskKey string `json:"psk_key"`
|
|
KeyGen string `json:"key_gen"`
|
|
VpnKeyPrev string `json:"vpn_key_prev"`
|
|
}
|
|
|
|
type peerRow struct {
|
|
PubKey string
|
|
IP string
|
|
PskKey string
|
|
Owner string
|
|
CreatedAt int64
|
|
}
|
|
|
|
func openDB(path string) (*sql.DB, error) {
|
|
dsn := "file:" + (&url.URL{Path: path}).EscapedPath() + "?_pragma=busy_timeout(5000)"
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Один соединение: SQLite всё равно пишет по одному, а так нет «database is locked» между
|
|
// горутинами и Tx не конкурирует с запросами вне транзакции.
|
|
db.SetMaxOpenConns(1)
|
|
if err := initSchema(db); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
func initSchema(db *sql.DB) error {
|
|
for _, q := range []string{
|
|
`CREATE TABLE IF NOT EXISTS users (
|
|
name TEXT PRIMARY KEY,
|
|
ip TEXT NOT NULL UNIQUE,
|
|
pub_key TEXT NOT NULL,
|
|
vpn_key TEXT NOT NULL,
|
|
psk_key TEXT NOT NULL DEFAULT ''
|
|
)`,
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_ip ON users (ip)",
|
|
} {
|
|
if _, err := db.Exec(q); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// ALTER TABLE идемпотентным не бывает — смотрим фактический список колонок.
|
|
cols := map[string]bool{}
|
|
rows, err := db.Query("PRAGMA table_info(users)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
var cid int
|
|
var name, typ string
|
|
var notnull, pk int
|
|
var dflt sql.NullString
|
|
if err := rows.Scan(&cid, &name, &typ, ¬null, &dflt, &pk); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
cols[name] = true
|
|
}
|
|
rows.Close()
|
|
if !cols["key_gen"] {
|
|
if _, err := db.Exec("ALTER TABLE users ADD COLUMN key_gen TEXT NOT NULL DEFAULT '2'"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !cols["vpn_key_prev"] {
|
|
if _, err := db.Exec("ALTER TABLE users ADD COLUMN vpn_key_prev TEXT NOT NULL DEFAULT ''"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, q := range []string{
|
|
`CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
)`,
|
|
// Пиры устройств sen://: приватного ключа здесь нет и быть не может.
|
|
`CREATE TABLE IF NOT EXISTS peers (
|
|
pub_key TEXT PRIMARY KEY,
|
|
ip TEXT NOT NULL UNIQUE,
|
|
psk_key TEXT NOT NULL,
|
|
owner TEXT NOT NULL DEFAULT '',
|
|
created_at INTEGER NOT NULL
|
|
)`,
|
|
} {
|
|
if _, err := db.Exec(q); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type queryer interface {
|
|
Query(query string, args ...any) (*sql.Rows, error)
|
|
QueryRow(query string, args ...any) *sql.Row
|
|
Exec(query string, args ...any) (sql.Result, error)
|
|
}
|
|
|
|
func (s *Server) getCfg(key, fallback string) string {
|
|
var v string
|
|
if err := s.db.QueryRow("SELECT value FROM config WHERE key = ?", key).Scan(&v); err != nil {
|
|
return fallback
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (s *Server) setCfg(key, value string) {
|
|
if _, err := s.db.Exec("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", key, value); err != nil {
|
|
s.log.Error("setCfg failed", "key", key, "error", err)
|
|
}
|
|
}
|
|
|
|
const userCols = "name, ip, pub_key, vpn_key, psk_key, key_gen, vpn_key_prev"
|
|
|
|
func scanUsers(rows *sql.Rows) ([]userRow, error) {
|
|
defer rows.Close()
|
|
out := []userRow{}
|
|
for rows.Next() {
|
|
var u userRow
|
|
if err := rows.Scan(&u.Name, &u.IP, &u.PubKey, &u.VpnKey, &u.PskKey, &u.KeyGen, &u.VpnKeyPrev); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, u)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Server) usersAll() ([]userRow, error) {
|
|
rows, err := s.db.Query("SELECT " + userCols + " FROM users")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return scanUsers(rows)
|
|
}
|
|
|
|
func (s *Server) userGet(name string) (*userRow, error) {
|
|
rows, err := s.db.Query("SELECT "+userCols+" FROM users WHERE name = ?", name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
us, err := scanUsers(rows)
|
|
if err != nil || len(us) == 0 {
|
|
return nil, err
|
|
}
|
|
return &us[0], nil
|
|
}
|
|
|
|
func scanPeers(rows *sql.Rows) ([]peerRow, error) {
|
|
defer rows.Close()
|
|
out := []peerRow{}
|
|
for rows.Next() {
|
|
var p peerRow
|
|
if err := rows.Scan(&p.PubKey, &p.IP, &p.PskKey, &p.Owner, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Server) peersAll() ([]peerRow, error) {
|
|
rows, err := s.db.Query("SELECT pub_key, ip, psk_key, owner, created_at FROM peers ORDER BY created_at, rowid")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return scanPeers(rows)
|
|
}
|
|
|
|
func (s *Server) peerGet(pub string) (*peerRow, error) {
|
|
rows, err := s.db.Query("SELECT pub_key, ip, psk_key, owner, created_at FROM peers WHERE pub_key = ?", pub)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ps, err := scanPeers(rows)
|
|
if err != nil || len(ps) == 0 {
|
|
return nil, err
|
|
}
|
|
return &ps[0], nil
|
|
}
|
|
|
|
func (s *Server) userPubTaken(q queryer, pub string) (bool, error) {
|
|
var n int
|
|
if err := q.QueryRow("SELECT COUNT(*) AS n FROM users WHERE pub_key = ?", pub).Scan(&n); err != nil {
|
|
return false, err
|
|
}
|
|
return n > 0, nil
|
|
}
|
|
|
|
// nextIp: первый свободный 10.9.c.d (c = 0..255, d = 2..254) среди users и peers.
|
|
func (s *Server) nextIp(q queryer, subnet string) (string, error) {
|
|
rows, err := q.Query("SELECT ip FROM users UNION SELECT ip FROM peers")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
used := map[string]bool{}
|
|
for rows.Next() {
|
|
var ip string
|
|
if err := rows.Scan(&ip); err != nil {
|
|
rows.Close()
|
|
return "", err
|
|
}
|
|
used[ip] = true
|
|
}
|
|
rows.Close()
|
|
for c := 0; c <= 255; c++ {
|
|
for d := 2; d <= 254; d++ {
|
|
ip := fmt.Sprintf("%s.%d.%d", subnet, c, d)
|
|
if !used[ip] {
|
|
return ip, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("Подсеть заполнена")
|
|
}
|