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,186 @@
|
||||
// Package ctrl — awg-ctrl: привилегированный REST API, который управляет интерфейсом
|
||||
// AmneziaWG (`awg`, `awg-quick`, `ip`, `tc`, `nft`) и базой пользователей users.db.
|
||||
// Слушает только 127.0.0.1; снаружи до него добирается лишь awg-ui (и агент ноды, и CLI)
|
||||
// с подписанным Ed25519-токеном.
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/maeneko/forgetting/internal/iauth"
|
||||
"github.com/maeneko/forgetting/internal/logx"
|
||||
)
|
||||
|
||||
// Config — то, что в Node-версии лежало в CONFIG. Параметры и поколение меняются при
|
||||
// restartAwg/переходе на 3.1 — читать через Server.cfg().
|
||||
type Config struct {
|
||||
ServerIP string
|
||||
ServerPort int
|
||||
ServerName string
|
||||
DNS1, DNS2 string
|
||||
MTU int
|
||||
Keepalive string
|
||||
Subnet string
|
||||
Params Params
|
||||
Gen string
|
||||
AwgModule string
|
||||
AwgTools string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
iface string
|
||||
confDir string
|
||||
dbPath string
|
||||
serverPrivKeyFile string
|
||||
port int
|
||||
|
||||
db *sql.DB
|
||||
log *logx.Logger
|
||||
pub ed25519.PublicKey
|
||||
|
||||
// mu делает последовательности «БД + awg + awg1.conf» неделимыми. В Node весь код между
|
||||
// await атомарен (один поток); в Go без этого два запроса перемешали бы шаги.
|
||||
mu sync.Mutex
|
||||
cfgMu sync.RWMutex
|
||||
c Config
|
||||
|
||||
shaping *Shaping
|
||||
metrics metricsCache
|
||||
monitor monitorJob
|
||||
cliPath string
|
||||
cliLog string
|
||||
}
|
||||
|
||||
func (s *Server) cfg() Config {
|
||||
s.cfgMu.RLock()
|
||||
defer s.cfgMu.RUnlock()
|
||||
return s.c
|
||||
}
|
||||
|
||||
func (s *Server) keyEnv() keyEnv {
|
||||
c := s.cfg()
|
||||
return keyEnv{P: c.Params, ServerIP: c.ServerIP, ServerPort: c.ServerPort, ServerName: c.ServerName,
|
||||
DNS1: c.DNS1, DNS2: c.DNS2, MTU: c.MTU, Keepalive: c.Keepalive, Gen: c.Gen, Subnet: c.Subnet}
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
r := runCmd("ip", "route", "show", "default")
|
||||
m := regexp.MustCompile(`dev\s+(\S+)`).FindStringSubmatch(r.Stdout)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
a := runCmd("ip", "addr", "show", m[1])
|
||||
if m := regexp.MustCompile(`inet\s+([\d.]+)`).FindStringSubmatch(a.Stdout); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// New открывает базу и ключи, читает параметры из awg1.conf. Не слушает порт.
|
||||
func New() (*Server, error) {
|
||||
s := &Server{
|
||||
iface: "awg1",
|
||||
confDir: envOr("AWG_CONF_DIR", "/etc/amnezia/amneziawg"),
|
||||
serverPrivKeyFile: envOr("AWG_SERVER_PRIV_KEY_FILE", "/etc/amnezia/server_private.key"),
|
||||
cliPath: envOr("AWG_CLI", "/usr/local/bin/awg-ctrl"),
|
||||
}
|
||||
s.dbPath = envOr("AWG_DB_PATH", filepath.Join(s.confDir, "users.db"))
|
||||
port, _ := strconv.Atoi(os.Getenv("PORT"))
|
||||
if port == 0 {
|
||||
port = 3005
|
||||
}
|
||||
s.port = port
|
||||
cwd, _ := os.Getwd()
|
||||
s.log = logx.New(os.Stdout, filepath.Join(cwd, "logs"))
|
||||
s.cliLog = envOr("MONITOR_LOG", filepath.Join(filepath.Dir(cwd), "logs", "monitor-job.log"))
|
||||
|
||||
pubFile := envOr("INTERNAL_AUTH_PUB_FILE", "/etc/amnezia/amneziawg/internal_auth_public.key")
|
||||
pub, err := iauth.LoadPublic(pubFile)
|
||||
if err != nil {
|
||||
s.log.Error("FATAL: публичный ключ внутренней авторизации не найден: " + pubFile)
|
||||
return nil, fmt.Errorf("FATAL: публичный ключ внутренней авторизации не найден: %s", pubFile)
|
||||
}
|
||||
s.pub = pub
|
||||
|
||||
db, err := openDB(s.dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.db = db
|
||||
|
||||
// Адрес/порт/имя: БД, затем env, затем (для адреса) маршрут по умолчанию. Как в Node,
|
||||
// запасной адрес вычисляется всегда, даже если в БД уже есть значение.
|
||||
fallbackIP := ""
|
||||
if v, ok := os.LookupEnv("SERVER_IP"); ok {
|
||||
fallbackIP = v
|
||||
} else {
|
||||
fallbackIP = getLocalIP()
|
||||
}
|
||||
ip := s.getCfg("serverIp", fallbackIP)
|
||||
portStr := s.getCfg("serverPort", envOr("SERVER_PORT", "51820"))
|
||||
name := s.getCfg("serverName", envOr("SERVER_NAME", "VPN"))
|
||||
if ip == "" {
|
||||
return nil, fmt.Errorf("SERVER_IP не задан — передай через env при первом запуске")
|
||||
}
|
||||
s.setCfg("serverIp", ip)
|
||||
s.setCfg("serverPort", portStr)
|
||||
s.setCfg("serverName", name)
|
||||
sp, _ := strconv.Atoi(portStr)
|
||||
|
||||
params := ReadParams(s.confPath(), func(m string) { s.log.Warn(m) })
|
||||
gen := params.Gen()
|
||||
module, tools := readVersions()
|
||||
s.c = Config{
|
||||
ServerIP: ip, ServerPort: sp, ServerName: name,
|
||||
DNS1: "1.1.1.1", DNS2: "1.0.0.1", MTU: 1376, Subnet: "10.9",
|
||||
Keepalive: keepaliveByGen[gen], Params: params, Gen: gen,
|
||||
AwgModule: module, AwgTools: tools,
|
||||
}
|
||||
s.log.Info("awg params loaded from conf", "Jc", params.Jc, "H1", params.H1, "gen", gen)
|
||||
s.shaping = newShaping(s)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Run слушает 127.0.0.1:PORT и выполняет стартовую последовательность Node-версии:
|
||||
// listen → ensureInterfaceUp → syncPeers → шейпинг. Нет интерфейса — завершаемся с ошибкой:
|
||||
// health-gate CLI опирается на это (без поднятого awg1 awg-ctrl не нужен).
|
||||
func (s *Server) Run() error {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv := &http.Server{Handler: s.handler(), ReadHeaderTimeout: 15 * time.Second}
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- srv.Serve(ln) }()
|
||||
c := s.cfg()
|
||||
s.log.Info("Server started", "port", s.port, "host", "127.0.0.1", "serverName", c.ServerName, "serverIp", c.ServerIP)
|
||||
|
||||
if err := s.ensureInterfaceUp(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.syncPeers(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Обязательно и на старте: при загрузке интерфейс поднимает systemd (ExecStartPre),
|
||||
// ни один из путей awg-quick up внутри awg-ctrl тогда не срабатывает.
|
||||
go s.shaping.Apply()
|
||||
s.shaping.Start()
|
||||
return <-errc
|
||||
}
|
||||
Reference in New Issue
Block a user