Start the Go rewrite: jsjson, awg-ctrl port, differential bench
- 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
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/maeneko/forgetting/internal/jsjson"
|
||||
)
|
||||
|
||||
func nowFn() time.Time { return time.Now() }
|
||||
|
||||
type ob = jsjson.Object
|
||||
type kv = jsjson.KV
|
||||
|
||||
func ok(v any) (result, error) { return result{200, v}, nil }
|
||||
func created(v any) (result, error) { return result{201, v}, nil }
|
||||
func (s *Server) routes() []route {
|
||||
a := []func(*rctx) error{s.auth}
|
||||
an := []func(*rctx) error{s.auth, validateName}
|
||||
R := func(method, path string, lock bool, mw []func(*rctx) error, h func(*rctx) (result, error)) route {
|
||||
segs := strings.Split(strings.Trim(path, "/"), "/")
|
||||
return route{method: method, segs: segs, mw: mw, lock: lock, h: h}
|
||||
}
|
||||
|
||||
return []route{
|
||||
R("GET", "/health", false, nil, func(c *rctx) (result, error) {
|
||||
st := s.interfaceStatus()
|
||||
cfg := s.cfg()
|
||||
status, word := 200, "ok"
|
||||
awgSt := "ok"
|
||||
if !st.Up {
|
||||
status, word, awgSt = 503, "degraded", "down"
|
||||
}
|
||||
return result{status, ob{{"status", word}, {"server", cfg.ServerName}, {"ip", cfg.ServerIP}, {"gen", cfg.Gen},
|
||||
{"awg", ob{{"status", awgSt}, {"peers", st.Peers}, {"module", cfg.AwgModule}, {"tools", cfg.AwgTools}}}}}, nil
|
||||
}),
|
||||
|
||||
R("POST", "/api/users", true, an, func(c *rctx) (result, error) {
|
||||
name, _ := c.obj().Get2("name").(string)
|
||||
if u, err := s.userGet(name); err != nil {
|
||||
return result{}, err
|
||||
} else if u != nil {
|
||||
return result{}, httpErr(409, "Пользователь уже существует")
|
||||
}
|
||||
u, err := s.addUser(name)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return created(u)
|
||||
}),
|
||||
|
||||
R("GET", "/api/users", true, a, func(c *rctx) (result, error) {
|
||||
rows, err := s.db.Query("SELECT name, ip, pub_key, vpn_key, key_gen FROM users WHERE vpn_key != ''")
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
stats := s.getPeersData()
|
||||
list := []any{}
|
||||
for rows.Next() {
|
||||
var n, ip, pub, vk, kg string
|
||||
if err := rows.Scan(&n, &ip, &pub, &vk, &kg); err != nil {
|
||||
rows.Close()
|
||||
return result{}, err
|
||||
}
|
||||
ps := stats[pub]
|
||||
list = append(list, ob{{"name", n}, {"ip", ip}, {"pub_key", pub}, {"vpn_key", vk}, {"key_gen", kg},
|
||||
{"online", ps.Online}, {"lastHandshake", ps.LastHandshake}})
|
||||
}
|
||||
rows.Close()
|
||||
return ok(ob{{"users", list}})
|
||||
}),
|
||||
|
||||
R("GET", "/api/users/stats", true, a, func(c *rctx) (result, error) {
|
||||
rows, err := s.db.Query("SELECT name, ip, pub_key, key_gen FROM users WHERE vpn_key != ''")
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
stats := s.getPeersData()
|
||||
list := []any{}
|
||||
for rows.Next() {
|
||||
var n, ip, pub, kg string
|
||||
if err := rows.Scan(&n, &ip, &pub, &kg); err != nil {
|
||||
rows.Close()
|
||||
return result{}, err
|
||||
}
|
||||
ps := stats[pub]
|
||||
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)}})
|
||||
}
|
||||
rows.Close()
|
||||
return ok(ob{{"users", list}})
|
||||
}),
|
||||
|
||||
// ⚠️ До «/api/users/:name», иначе тот примет «reissue» за имя пользователя.
|
||||
R("POST", "/api/users/reissue", true, a, func(c *rctx) (result, error) {
|
||||
r, err := s.reissueAll()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(r)
|
||||
}),
|
||||
|
||||
R("POST", "/api/users/:name", true, an, func(c *rctx) (result, error) {
|
||||
u, err := s.userGet(c.params["name"])
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
if u == nil {
|
||||
return result{}, httpErr(404, "Пользователь не найден")
|
||||
}
|
||||
return ok(u)
|
||||
}),
|
||||
|
||||
R("DELETE", "/api/users/:name", true, an, func(c *rctx) (result, error) {
|
||||
if err := s.removeUser(c.params["name"]); err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(ob{{"success", true}, {"name", c.params["name"]}})
|
||||
}),
|
||||
|
||||
R("POST", "/api/peers", true, a, func(c *rctx) (result, error) {
|
||||
owner, isStr := c.obj().Get2("owner").(string)
|
||||
if !isStr || !reOwner.MatchString(owner) {
|
||||
return result{}, httpErr(400, "Неверный owner")
|
||||
}
|
||||
pub, err := checkPubKeyAny(c.obj().Get2("pub_key"))
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
r, err := s.addPeer(pub, owner)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return created(r)
|
||||
}),
|
||||
|
||||
R("GET", "/api/peers", true, a, func(c *rctx) (result, error) {
|
||||
stats := s.getPeersData()
|
||||
peers, err := s.peersAll()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
list := []any{}
|
||||
for _, p := range peers {
|
||||
ps := stats[p.PubKey]
|
||||
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},
|
||||
{"priority", s.shaping.PriorityOf(p.IP)}})
|
||||
}
|
||||
return ok(ob{{"peers", list}})
|
||||
}),
|
||||
|
||||
// Пиры, которые стоят в ядре, но неизвестны awg-ctrl (нет ни в users, ни в peers): снять
|
||||
// их после сбоя `awg set … remove` некому. Только чтение — панель сама решает, что делать.
|
||||
R("GET", "/api/audit", true, a, func(c *rctx) (result, error) {
|
||||
r := runCmd("awg", "show", s.iface, "dump")
|
||||
if !r.ok() {
|
||||
return result{}, httpErr(503, "awg show не ответил")
|
||||
}
|
||||
known, err := s.knownPubKeys()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
now := float64(time.Now().Unix())
|
||||
ghosts := []any{}
|
||||
lines := strings.Split(r.Stdout, "\n")
|
||||
for _, l := range lines[1:] {
|
||||
f := strings.Split(l, "\t")
|
||||
if f[0] == "" || known[f[0]] {
|
||||
continue
|
||||
}
|
||||
num := func(i int) float64 {
|
||||
if i < len(f) {
|
||||
return orZero(jsNumber(f[i]))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
hs := num(4)
|
||||
ghosts = append(ghosts, ob{{"pub_key", f[0]}, {"lastHandshake", hs}, {"online", hs > 0 && now-hs < 180}, {"rx", num(5)}, {"tx", num(6)}})
|
||||
}
|
||||
return ok(ob{{"ghosts", ghosts}})
|
||||
}),
|
||||
|
||||
// Снять «призраков» из ядра. Ключи присылает панель, но здесь каждый перепроверяется:
|
||||
// пир, который успел появиться в users/peers, не трогаем. Строк в базе у «призраков»
|
||||
// нет — снимается только ядро.
|
||||
R("POST", "/api/audit/clean", true, a, func(c *rctx) (result, error) {
|
||||
arr, isArr := c.obj().Get2("pub_keys").([]any)
|
||||
if !isArr || len(arr) > syncMax {
|
||||
return result{}, httpErr(400, "Неверный список ключей")
|
||||
}
|
||||
keys, err := checkPubKeys(arr)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
removed, skipped := []string{}, []string{}
|
||||
for _, k := range keys {
|
||||
known, err := s.isKnownPub(k)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
skip := known
|
||||
if !skip {
|
||||
sp, err := s.getServerPublicKey()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
kp := s.kernelPeers()
|
||||
skip = k == sp || kp == nil || !kp[k]
|
||||
}
|
||||
if skip {
|
||||
skipped = append(skipped, k)
|
||||
} else if s.dropFromKernel(k) {
|
||||
removed = append(removed, k)
|
||||
} else {
|
||||
skipped = append(skipped, k)
|
||||
}
|
||||
}
|
||||
if len(removed) > 0 {
|
||||
s.log.Warn("ghost peers removed from kernel", "count", len(removed), "removed", removed)
|
||||
}
|
||||
return ok(ob{{"removed", removed}, {"skipped", skipped}})
|
||||
}),
|
||||
|
||||
// Должен стоять ДО PUT /api/peers/:pub — иначе тот примет «sync» за ключ.
|
||||
R("PUT", "/api/peers/sync", true, a, func(c *rctx) (result, error) {
|
||||
owner, isStr := c.obj().Get2("owner").(string)
|
||||
if !isStr || !rePanelID.MatchString(owner) {
|
||||
return result{}, httpErr(400, "Неверный owner")
|
||||
}
|
||||
arr, isArr := c.obj().Get2("keep").([]any)
|
||||
if !isArr || len(arr) > syncMax {
|
||||
return result{}, httpErr(400, "Неверный список ключей")
|
||||
}
|
||||
keep, err := checkPubKeys(arr)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
r, err := s.syncOwnedPeers(owner, keep)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(r)
|
||||
}),
|
||||
|
||||
R("PUT", "/api/peers/:pub", true, a, func(c *rctx) (result, error) {
|
||||
oldPub, err := pubKeyFromParam(c.params["pub"])
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
newPub, err := checkPubKeyAny(c.obj().Get2("pub_key"))
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
r, err := s.replacePeer(oldPub, newPub)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(r)
|
||||
}),
|
||||
|
||||
R("POST", "/api/peers/:pub/psk", true, a, func(c *rctx) (result, error) {
|
||||
pub, err := pubKeyFromParam(c.params["pub"])
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
r, err := s.rotatePeerPSK(pub)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(r)
|
||||
}),
|
||||
|
||||
R("DELETE", "/api/peers/:pub", true, a, func(c *rctx) (result, error) {
|
||||
pub, err := pubKeyFromParam(c.params["pub"])
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
if err := s.removePeer(pub); err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(ob{{"success", true}})
|
||||
}),
|
||||
|
||||
// Общая часть клиентского конфига для подписки. Ключи obfuscation — именами .conf;
|
||||
// пустые (I2–I5, весь 3.x-блок на 2.0) не выводятся, как и в самом conf.
|
||||
R("GET", "/api/profile", true, a, func(c *rctx) (result, error) {
|
||||
sp, err := s.getServerPublicKey()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
cfg := s.cfg()
|
||||
return ok(ob{{"name", cfg.ServerName}, {"endpoint", cfg.ServerIP + ":" + strconv.Itoa(cfg.ServerPort)},
|
||||
{"server_pub", sp}, {"gen", cfg.Gen}, {"dns", []string{cfg.DNS1, cfg.DNS2}},
|
||||
{"keepalive", cfg.Keepalive}, {"mtu", cfg.MTU}, {"awg", profileAwg(cfg.Params)}})
|
||||
}),
|
||||
|
||||
R("POST", "/awg/restart", true, a, func(c *rctx) (result, error) {
|
||||
if err := s.restartAwg(); err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(ob{{"success", true}})
|
||||
}),
|
||||
|
||||
// Перевод сервера с 2.0 на 3.1 — включая перевыпуск всех vpn:// ключей. Ошибку отдаём
|
||||
// текстом: панель показывает её оператору, а причины отказа (старый модуль, старое ядро,
|
||||
// отказ ядра на подъёме) требуют разных действий.
|
||||
R("POST", "/awg/upgrade", true, a, func(c *rctx) (result, error) {
|
||||
if s.cfg().Gen == "3.1" {
|
||||
return result{}, httpErr(409, "Сервер уже работает на AmneziaWG 3.1")
|
||||
}
|
||||
if blocker := s.checkGen31Support(); blocker != "" {
|
||||
return result{}, httpErr(409, blocker)
|
||||
}
|
||||
r, err := s.upgradeToGen31()
|
||||
if err != nil {
|
||||
return result{}, httpErr(500, err.Error())
|
||||
}
|
||||
return ok(r)
|
||||
}),
|
||||
|
||||
// Деление канала между устройствами. Путь под /api/ — иначе awg-agent на ноде его не
|
||||
// пропустит (и без PATCH: агент отвечает на него 400).
|
||||
R("GET", "/api/shaping", false, a, func(c *rctx) (result, error) { return ok(s.shaping.Status()) }),
|
||||
|
||||
R("PUT", "/api/shaping", false, a, func(c *rctx) (result, error) {
|
||||
b := c.obj()
|
||||
enabled, e1 := b.Get2("enabled").(bool)
|
||||
auto, e2 := b.Get2("auto").(bool)
|
||||
if !e1 || !e2 {
|
||||
return result{}, httpErr(400, "enabled и auto должны быть true/false")
|
||||
}
|
||||
mbit := func(v any, what string) (int, error) {
|
||||
n := jsToNumber(v)
|
||||
if math.IsNaN(n) || n != math.Trunc(n) || n < 0 || n > 100_000 {
|
||||
return 0, httpErr(400, what+": целое число Мбит/с от 1 до 100000")
|
||||
}
|
||||
return int(n), nil
|
||||
}
|
||||
down, err := mbit(b.Get2("downMbit"), "Загрузка")
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
up, err := mbit(b.Get2("upMbit"), "Отдача")
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
if enabled && (down < 1 || up < 1) {
|
||||
return result{}, httpErr(400, "Для включения укажите ширину канала в обе стороны")
|
||||
}
|
||||
// tc/nft/модуль не нашлись или ядро отказало: оператору нужна причина, а не 500.
|
||||
if err := s.shaping.Update(ShapingSettings{Enabled: enabled, DownMbit: down, UpMbit: up, Auto: auto}); err != nil {
|
||||
return result{}, httpErr(500, err.Error())
|
||||
}
|
||||
return ok(s.shaping.Status())
|
||||
}),
|
||||
|
||||
R("GET", "/api/metrics", false, a, func(c *rctx) (result, error) {
|
||||
d, err := s.metricsCached()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(d)
|
||||
}),
|
||||
|
||||
R("POST", "/api/monitor/install", false, a, func(c *rctx) (result, error) {
|
||||
if err := s.startMonitorJob("install"); err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return result{202, ob{{"running", true}, {"action", "install"}}}, nil
|
||||
}),
|
||||
R("POST", "/api/monitor/remove", false, a, func(c *rctx) (result, error) {
|
||||
if err := s.startMonitorJob("remove"); err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return result{202, ob{{"running", true}, {"action", "remove"}}}, nil
|
||||
}),
|
||||
R("GET", "/api/monitor/job", false, a, func(c *rctx) (result, error) { return ok(s.monitorView()) }),
|
||||
|
||||
R("GET", "/awg/status", true, a, func(c *rctx) (result, error) { return ok(s.interfaceStatus()) }),
|
||||
|
||||
R("POST", "/awg/start", true, a, func(c *rctx) (result, error) {
|
||||
st, err := s.startInterface()
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return ok(st)
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user