awg-ctrl: commands for the secret path, connection log, metrics, hub, check
The new features were only reachable from the web panel. awg-ctrl now has: - panel [path new|off|<path>] — the panel address; a new random, own or no secret path (cli.env, panel restarted, VPN untouched); - log [<search>] [-n N] and log seen — the connection log of this server and who was online when, with sen:// device and master key names from ui.db when the panel is on the same machine; - metrics [token|revoke] — the metrics-only token for an external Prometheus, printed once with a ready prometheus.yml fragment that carries the path; - hub [legacy on|off|auto] — nodes with and without the pre-upgrade signature and the HUB_LEGACY mode; - check — what this server gives away: shared pre-randomization VPN values or no own I1, no secret path, plain-HTTP panel and subscription, the generic "sen"/"awg-node" certificate names, an open hub — each with the fix. The interactive menu gets items 10–13: panel address and path, connection log, who was online, and the check. Columns are padded by runes, not bytes, so Cyrillic names line up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
package cli
|
||||
|
||||
// Команды для новых возможностей панели — то же, что в веб-интерфейсе, но с сервера:
|
||||
// panel [path new|off|<путь>] — адрес панели, секретный путь (UI_PREFIX)
|
||||
// log [seen] [поиск] [-n N] — журнал VPN-подключений этого сервера, «кто когда был»
|
||||
// metrics [token|revoke] — токен «только метрики» для своего Prometheus
|
||||
// hub [legacy on|off|auto] — хаб нод: подпись до апгрейда, кто из нод ещё старый
|
||||
// check — что на этом сервере выдаёт его сканеру и DPI
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/maeneko/forgetting/internal/migrate"
|
||||
)
|
||||
|
||||
// pad — дополнение пробелами по числу символов, не байт: в Printf %-Ns кириллица съезжает.
|
||||
func pad(s string, n int) string {
|
||||
if l := len([]rune(s)); l < n {
|
||||
return s + strings.Repeat(" ", n-l)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func yellow(s string) string { return "\x1b[33m" + s + cReset }
|
||||
|
||||
func (c *CLI) hasPanel() bool { return contains(c.all, "ui") || env("UI_PORT", "") != "" && !c.isNode }
|
||||
func (c *CLI) hasVPN() bool { return contains(c.all, "awgctrl") }
|
||||
|
||||
func (c *CLI) uiDB() (*sql.DB, error) {
|
||||
f := c.svcEnv("ui", "UI_DB_FILE")
|
||||
if !exists(f) {
|
||||
return nil, fmt.Errorf("базы панели нет (%s)", f)
|
||||
}
|
||||
return migrate.OpenDB(f)
|
||||
}
|
||||
|
||||
// restartUI — cli.env изменён: панель перезапускается с новым значением, VPN не трогаем.
|
||||
func (c *CLI) restartUI(key, value string) {
|
||||
c.setSvcEnv("ui", key, value)
|
||||
if c.isRunning("ui") {
|
||||
c.stop("ui")
|
||||
c.start("ui")
|
||||
}
|
||||
}
|
||||
|
||||
func when(ts int64) string {
|
||||
t := time.Unix(ts, 0)
|
||||
now := time.Now()
|
||||
switch {
|
||||
case t.YearDay() == now.YearDay() && t.Year() == now.Year():
|
||||
return "сегодня " + t.Format("15:04")
|
||||
case t.Year() == now.Year():
|
||||
return t.Format("02.01 15:04")
|
||||
}
|
||||
return t.Format("02.01.2006 15:04")
|
||||
}
|
||||
|
||||
func dur(s int64) string {
|
||||
switch {
|
||||
case s < 60:
|
||||
return fmt.Sprintf("%d сек", s)
|
||||
case s < 3600:
|
||||
return fmt.Sprintf("%d мин", s/60)
|
||||
case s < 86400:
|
||||
return fmt.Sprintf("%d ч %d мин", s/3600, s%3600/60)
|
||||
}
|
||||
return fmt.Sprintf("%d дн %d ч", s/86400, s%86400/3600)
|
||||
}
|
||||
|
||||
func bytesStr(n float64) string {
|
||||
switch {
|
||||
case n < 1<<10:
|
||||
return fmt.Sprintf("%.0f B", n)
|
||||
case n < 1<<20:
|
||||
return fmt.Sprintf("%.1f KB", n/(1<<10))
|
||||
case n < 1<<30:
|
||||
return fmt.Sprintf("%.1f MB", n/(1<<20))
|
||||
}
|
||||
return fmt.Sprintf("%.2f GB", n/(1<<30))
|
||||
}
|
||||
|
||||
// ── panel ───────────────────────────────────────────────────────────────────
|
||||
|
||||
var rePrefix = regexp.MustCompile(`^[A-Za-z0-9_-]{8,64}$`)
|
||||
|
||||
func randPrefix() string {
|
||||
const abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
for i := range b {
|
||||
b[i] = abc[int(b[i])%len(abc)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (c *CLI) panelCmd(args []string) {
|
||||
if !c.hasPanel() {
|
||||
fmt.Println(" Панели на этой машине нет — это нода")
|
||||
return
|
||||
}
|
||||
if len(args) == 0 {
|
||||
c.showPanel()
|
||||
return
|
||||
}
|
||||
if args[0] != "path" || len(args) < 2 {
|
||||
errorf(" Usage: awg-ctrl panel [path new|off|<путь>]")
|
||||
os.Exit(1)
|
||||
}
|
||||
c.setPanelPath(args[1])
|
||||
}
|
||||
|
||||
func (c *CLI) showPanel() {
|
||||
fmt.Printf("\n Панель %s\n", bold(panelURL()))
|
||||
if p := env("UI_PREFIX", ""); p != "" {
|
||||
fmt.Println(" " + dim("Секретный путь: снаружи без него — 404. Сменить: awg-ctrl panel path new"))
|
||||
} else {
|
||||
fmt.Println(" " + yellow("Без секретного пути: сканер узнаёт панель по странице логина.") + " " + dim("Включить: awg-ctrl panel path new"))
|
||||
}
|
||||
if m := env("UI_PREFIX_MOVED", ""); m != "" {
|
||||
fmt.Println(" " + dim("Принимается и путь переехавшей панели: /"+m+"/"))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// setPanelPath — new (случайный), off (без пути) или свой. Закладки и адрес внешнего API
|
||||
// со старым путём перестанут работать — предупреждаем.
|
||||
func (c *CLI) setPanelPath(arg string) {
|
||||
v := arg
|
||||
switch arg {
|
||||
case "new":
|
||||
v = randPrefix()
|
||||
case "off":
|
||||
v = ""
|
||||
default:
|
||||
if !rePrefix.MatchString(arg) {
|
||||
errorf(" Путь: 8–64 символа A–Z a–z 0–9 _ -, или new / off")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if err := c.writeEnvFile(updateEnvVar(c.readEnvFile(), "UI_PREFIX", v)); err != nil {
|
||||
errorf(" Не удалось записать %s: %v", c.envFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Setenv("UI_PREFIX", v)
|
||||
c.restartUI("UI_PREFIX", v)
|
||||
fmt.Printf("\n %s Панель: %s\n", green("✓"), bold(panelURL()))
|
||||
fmt.Println(" " + dim("Старый адрес больше не открывается: обновите закладки и адрес внешнего API и Prometheus."))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ── log ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
var reOwner = regexp.MustCompile(`^(?:([a-z0-9]{8,16})/)?m(\d+)/d(\d+)$`)
|
||||
|
||||
// ownerName — «устройство · ключ» для пира sen://, если его панель — на этой машине.
|
||||
func (c *CLI) ownerName(db *sql.DB, owner string) string {
|
||||
m := reOwner.FindStringSubmatch(owner)
|
||||
if m == nil || db == nil || m[1] != "" && m[1] != c.localPanelID() {
|
||||
return owner
|
||||
}
|
||||
var label, dev string
|
||||
db.QueryRow("SELECT label FROM master_keys WHERE id = ?", m[2]).Scan(&label)
|
||||
db.QueryRow("SELECT device_name FROM devices WHERE id = ?", m[3]).Scan(&dev)
|
||||
switch {
|
||||
case dev != "" && label != "":
|
||||
return dev + " · «" + label + "»"
|
||||
case label != "":
|
||||
return "удалённое устройство · «" + label + "»"
|
||||
}
|
||||
return "удалённое устройство"
|
||||
}
|
||||
|
||||
func (c *CLI) logCmd(args []string) {
|
||||
if !c.hasVPN() {
|
||||
fmt.Println(" VPN на этой машине нет — журнал серверов во вкладке «Подключения» панели")
|
||||
return
|
||||
}
|
||||
seen, limit, q := false, 50, ""
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch a := args[i]; {
|
||||
case a == "seen":
|
||||
seen = true
|
||||
case a == "-n" && i+1 < len(args):
|
||||
n, err := strconv.Atoi(args[i+1])
|
||||
if err != nil || n <= 0 {
|
||||
errorf(" -n: число событий")
|
||||
os.Exit(1)
|
||||
}
|
||||
limit, i = min(n, 500), i+1
|
||||
default:
|
||||
q = strings.TrimSpace(q + " " + a)
|
||||
}
|
||||
}
|
||||
v := url.Values{"limit": {strconv.Itoa(limit)}}
|
||||
if q != "" && !seen {
|
||||
v.Set("q", q)
|
||||
}
|
||||
r, err := c.ctrlCall("GET", "/api/connlog?"+v.Encode(), nil)
|
||||
if err != nil {
|
||||
errorf(" awg-ctrl недоступен: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if r.status == 404 {
|
||||
errorf(" awg-ctrl этой версии журнала не ведёт — обновите сервер: awg-ctrl update")
|
||||
os.Exit(1)
|
||||
}
|
||||
if r.status != 200 {
|
||||
errorf(" %v", ctrlError(r))
|
||||
os.Exit(1)
|
||||
}
|
||||
db, _ := c.uiDB()
|
||||
if db != nil {
|
||||
defer db.Close()
|
||||
}
|
||||
who := func(o any) (string, string) {
|
||||
name, _ := get(o, "name").(string)
|
||||
ip, _ := get(o, "ip").(string)
|
||||
if name == "" {
|
||||
if owner, _ := get(o, "owner").(string); owner != "" {
|
||||
name = c.ownerName(db, owner)
|
||||
} else {
|
||||
pub, _ := get(o, "pub_key").(string)
|
||||
name = "ключ " + pub[:min(10, len(pub))] + "…"
|
||||
}
|
||||
}
|
||||
return name, ip
|
||||
}
|
||||
fmt.Println()
|
||||
if since, ok := get(r.data, "since").(float64); ok {
|
||||
fmt.Printf(" %s\n\n", dim(fmt.Sprintf("Журнал ведётся с %s, хранится %v дней. Время отключения — последняя активность (±30 с).",
|
||||
when(int64(since)), get(r.data, "keep_days"))))
|
||||
}
|
||||
|
||||
if seen {
|
||||
type row struct {
|
||||
name, ip string
|
||||
online bool
|
||||
last int64
|
||||
}
|
||||
var rows []row
|
||||
peers, _ := get(r.data, "peers").([]any)
|
||||
for _, p := range peers {
|
||||
name, ip := who(p)
|
||||
if q != "" && !strings.Contains(strings.ToLower(name+" "+ip), strings.ToLower(q)) {
|
||||
continue
|
||||
}
|
||||
last, _ := get(p, "last_seen").(float64)
|
||||
online, _ := get(p, "online").(bool)
|
||||
rows = append(rows, row{name, ip, online, int64(last)})
|
||||
}
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
if rows[i].online != rows[j].online {
|
||||
return rows[i].online
|
||||
}
|
||||
return rows[i].last > rows[j].last
|
||||
})
|
||||
for _, x := range rows {
|
||||
state := dim("не подключался с начала журнала")
|
||||
switch {
|
||||
case x.online:
|
||||
state = green("онлайн")
|
||||
case x.last > 0:
|
||||
state = "был " + when(x.last) + dim(" ("+dur(time.Now().Unix()-x.last)+" назад)")
|
||||
}
|
||||
fmt.Printf(" %s %s %s\n", pad(x.name, 32), dim(pad(x.ip, 12)), state)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
fmt.Println(" " + dim("Никого"))
|
||||
}
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
events, _ := get(r.data, "events").([]any)
|
||||
if len(events) == 0 {
|
||||
fmt.Println(" " + dim("Событий нет"))
|
||||
}
|
||||
// Старые сверху: читается как лента.
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
e := events[i]
|
||||
at, _ := get(e, "at").(float64)
|
||||
name, ip := who(e)
|
||||
mark, tail := green("→ подключился"), ""
|
||||
if get(e, "event") == "disconnect" {
|
||||
mark = dim("← отключился ")
|
||||
d, _ := get(e, "duration").(float64)
|
||||
rx, _ := get(e, "rx").(float64)
|
||||
tx, _ := get(e, "tx").(float64)
|
||||
tail = dim(fmt.Sprintf(" %s · ↓ %s ↑ %s", dur(int64(d)), bytesStr(tx), bytesStr(rx)))
|
||||
}
|
||||
fmt.Printf(" %s %s %s %s%s\n", pad(when(int64(at)), 15), mark, name, dim(ip), tail)
|
||||
}
|
||||
fmt.Printf("\n %s\n\n", dim("Больше: awg-ctrl log -n 200 · по имени: awg-ctrl log <имя> · кто когда был: awg-ctrl log seen"))
|
||||
}
|
||||
|
||||
// ── metrics ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (c *CLI) metricsCmd(args []string) {
|
||||
if !c.hasPanel() {
|
||||
fmt.Println(" Панели на этой машине нет — токен метрик выдаёт панель")
|
||||
return
|
||||
}
|
||||
db, err := c.uiDB()
|
||||
if err != nil {
|
||||
errorf(" %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
sub = args[0]
|
||||
}
|
||||
switch sub {
|
||||
case "":
|
||||
var prefix string
|
||||
var created int64
|
||||
var last sql.NullInt64
|
||||
if db.QueryRow("SELECT prefix, created_at, last_used FROM metrics_token WHERE id = 1").Scan(&prefix, &created, &last) != nil {
|
||||
fmt.Print("\n Токена для внешнего Prometheus нет. Выдать: awg-ctrl metrics token\n\n")
|
||||
return
|
||||
}
|
||||
used := "ещё не было"
|
||||
if last.Valid && last.Int64 > 0 {
|
||||
used = when(last.Int64)
|
||||
}
|
||||
fmt.Printf("\n Токен %s · выдан %s · последний скрейп %s\n", bold(prefix), when(created), used)
|
||||
fmt.Print(" " + dim("Перевыпустить: awg-ctrl metrics token · отозвать: awg-ctrl metrics revoke") + "\n\n")
|
||||
case "token":
|
||||
b := make([]byte, 24)
|
||||
rand.Read(b)
|
||||
tok := "awgm_" + b64url.EncodeToString(b)
|
||||
h := sha256.Sum256([]byte(tok))
|
||||
if _, err := db.Exec("INSERT OR REPLACE INTO metrics_token (id, token_hash, prefix, created_at, last_used) VALUES (1, ?, ?, ?, NULL)",
|
||||
h[:], tok[:13]+"…", time.Now().Unix()); err != nil {
|
||||
errorf(" ui.db: %v (панель этой версии ещё не создала таблицу — запустите её)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
u, _ := url.Parse(panelURL())
|
||||
fmt.Printf("\n %s Токен (показывается один раз, прежний больше не работает):\n\n %s\n\n", green("✓"), bold(tok))
|
||||
fmt.Print(" Фрагмент prometheus.yml:\n\n")
|
||||
fmt.Printf(" scrape_configs:\n - job_name: vpn-panel\n metrics_path: %smetrics\n authorization:\n credentials: %s\n static_configs:\n - targets: ['%s']\n\n",
|
||||
u.Path, tok, u.Host)
|
||||
fmt.Print(" " + dim("По голому HTTP токен и метрики идут открытым текстом — скрейпьте через nginx с TLS или по VPN.") + "\n\n")
|
||||
case "revoke":
|
||||
db.Exec("DELETE FROM metrics_token WHERE id = 1")
|
||||
fmt.Printf(" %s Токен отозван\n", green("✓"))
|
||||
default:
|
||||
errorf(" Usage: awg-ctrl metrics [token|revoke]")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ── hub ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (c *CLI) hubCmd(args []string) {
|
||||
if !c.hasPanel() || env("NODE_PORT", "") == "" {
|
||||
fmt.Println(" Хаб нод на этой машине не включён (NODE_PORT)")
|
||||
return
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if args[0] != "legacy" || len(args) < 2 || !regexp.MustCompile(`^(on|off|auto)$`).MatchString(args[1]) {
|
||||
errorf(" Usage: awg-ctrl hub [legacy on|off|auto]")
|
||||
os.Exit(1)
|
||||
}
|
||||
v := args[1]
|
||||
if v == "auto" {
|
||||
v = ""
|
||||
}
|
||||
if err := c.writeEnvFile(updateEnvVar(c.readEnvFile(), "HUB_LEGACY", v)); err != nil {
|
||||
errorf(" %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Setenv("HUB_LEGACY", v)
|
||||
c.restartUI("HUB_LEGACY", v)
|
||||
fmt.Printf(" %s HUB_LEGACY=%s\n", green("✓"), args[1])
|
||||
}
|
||||
db, err := c.uiDB()
|
||||
if err != nil {
|
||||
errorf(" %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SELECT id, name, COALESCE(version, ''), COALESCE(last_seen, 0), COALESCE(preauth, 0), pub IS NOT NULL FROM nodes ORDER BY id")
|
||||
if err != nil {
|
||||
errorf(" ui.db: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
type node struct {
|
||||
id int64
|
||||
name, version string
|
||||
seen int64
|
||||
preauth, joined bool
|
||||
}
|
||||
var list []node
|
||||
for rows.Next() {
|
||||
var n node
|
||||
if rows.Scan(&n.id, &n.name, &n.version, &n.seen, &n.preauth, &n.joined) == nil {
|
||||
list = append(list, n)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
old := 0
|
||||
fmt.Printf("\n Хаб нод :%s\n\n", env("NODE_PORT", ""))
|
||||
for _, n := range list {
|
||||
state := green(pad("с подписью", 28))
|
||||
if !n.preauth {
|
||||
state, old = yellow(pad("старый агент — без подписи", 28)), old+1
|
||||
if !n.joined {
|
||||
state = dim(pad("ещё не подключалась", 28))
|
||||
}
|
||||
}
|
||||
seen := dim("не подключалась")
|
||||
if n.seen > 0 {
|
||||
seen = "был " + when(n.seen)
|
||||
}
|
||||
fmt.Printf(" #%-3d %s %s %s %s\n", n.id, pad(n.name, 20), dim(pad(n.version, 12)), state, seen)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
fmt.Println(" " + dim("Нод нет"))
|
||||
}
|
||||
mode := env("HUB_LEGACY", "")
|
||||
fmt.Println()
|
||||
switch {
|
||||
case mode == "on":
|
||||
fmt.Println(" " + yellow("HUB_LEGACY=on: хаб пускает агентов без подписи — сканер узнаёт его по /node/v1"))
|
||||
case mode == "off":
|
||||
fmt.Println(" HUB_LEGACY=off: только с подписью. Старые агенты не подключатся, пока ноду не обновят")
|
||||
case old > 0:
|
||||
fmt.Println(" " + yellow(fmt.Sprintf("Переходный режим: %d нод(ы) без подписи — хаб пускает и без неё. Обновите их: awg-ctrl update на ноде", old)))
|
||||
default:
|
||||
fmt.Println(" " + green("Все ноды с подписью — без неё хаб отвечает 404"))
|
||||
}
|
||||
fmt.Print(" " + dim("Принудительно: awg-ctrl hub legacy on|off|auto") + "\n\n")
|
||||
}
|
||||
|
||||
// ── check ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// Прежние общие параметры установок до рандомизации — по ним сервер узнаётся DPI.
|
||||
var legacyAWG = map[string]string{"ListenPort": "47619", "S1": "90", "S2": "45", "H1": "1224800044-2116730834"}
|
||||
|
||||
func readConf(file string) map[string]string {
|
||||
out := map[string]string{}
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, l := range strings.Split(strings.SplitN(string(raw), "[Peer]", 2)[0], "\n") {
|
||||
if k, v, ok := strings.Cut(l, "="); ok {
|
||||
out[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func certCN(file string) string {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
blk, _ := pem.Decode(raw)
|
||||
if blk == nil {
|
||||
return ""
|
||||
}
|
||||
crt, err := x509.ParseCertificate(blk.Bytes)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return crt.Subject.CommonName
|
||||
}
|
||||
|
||||
// checkCmd — сводка: что на этом сервере выдаёт его сканеру (активное зондирование) и DPI.
|
||||
func (c *CLI) checkCmd() {
|
||||
ok := func(s string) { fmt.Println(" " + green("✓") + " " + s) }
|
||||
bad := func(s, fix string) { fmt.Println(" " + yellow("⚠") + " " + s + "\n " + dim(fix)) }
|
||||
fmt.Println()
|
||||
if c.hasVPN() {
|
||||
conf := readConf(filepath.Join(env("AWG_CONF_DIR", "/etc/amnezia/amneziawg"), "awg1.conf"))
|
||||
switch {
|
||||
case conf == nil:
|
||||
bad("awg1.conf не прочитан", "запустите от root")
|
||||
default:
|
||||
var same []string
|
||||
for k, v := range legacyAWG {
|
||||
if conf[k] == v {
|
||||
same = append(same, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(same)
|
||||
if len(same) > 0 {
|
||||
bad("VPN: общие для старых установок значения ("+strings.Join(same, ", ")+") — по ним DPI находит все такие серверы сразу",
|
||||
"переустановка без сохранения данных (awg-ctrl reinstall) и раздача новых ключей")
|
||||
} else {
|
||||
ok("VPN: порт и параметры обфускации свои у этого сервера")
|
||||
}
|
||||
if conf["I1"] == "" {
|
||||
bad("VPN: нет своего I1 — ключи получают общий пакет-маскировку", "awg-ctrl reinstall с сохранением данных выдаст свой — старые ключи продолжат работать")
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.hasPanel() {
|
||||
if env("UI_PREFIX", "") != "" {
|
||||
ok("Панель: секретный путь — снаружи без него 404")
|
||||
} else {
|
||||
bad("Панель без секретного пути — сканер узнаёт её по странице логина", "awg-ctrl panel path new")
|
||||
}
|
||||
bad("Панель по голому HTTP: пути, JSON и токены видны DPI", "nginx с TLS перед панелью (README, «Панель за nginx») или вход по VPN")
|
||||
if strings.ToLower(env("SUB_TLS", "off")) == "on" {
|
||||
ok("Подписка sen:// по HTTPS")
|
||||
} else {
|
||||
bad("Подписка sen:// по HTTP: ответы подписаны, но видны DPI", "переустановка с SUB_TLS=on — ссылки придётся выдать заново")
|
||||
}
|
||||
if cn := certCN(c.svcEnv("ui", "SUB_TLS_CERT_FILE")); cn == "sen" {
|
||||
bad("Сертификат подписки с общим именем «sen»", "имя меняется только с ключом — при переустановке без сохранения данных")
|
||||
}
|
||||
if env("NODE_PORT", "") != "" {
|
||||
if cn := certCN(c.svcEnv("ui", "NODE_TLS_CERT_FILE")); cn == "awg-node" {
|
||||
bad("Сертификат хаба с общим именем «awg-node»", "имя меняется только с ключом — ноды придётся подключить заново")
|
||||
}
|
||||
switch strings.ToLower(env("HUB_LEGACY", "")) {
|
||||
case "on":
|
||||
bad("Хаб пускает агентов без подписи (HUB_LEGACY=on)", "awg-ctrl hub legacy auto")
|
||||
case "off":
|
||||
ok("Хаб: только агенты с подписью")
|
||||
default:
|
||||
ok("Хаб: подпись до апгрейда; кто из нод ещё без неё — awg-ctrl hub")
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Print("\n " + dim("Фаервол хостера (панель провайдера) отсюда не виден — открыты ли там лишние порты, проверьте сами.") + "\n\n")
|
||||
}
|
||||
|
||||
// panelMenu — пункт меню: адрес панели и смена секретного пути.
|
||||
func (c *CLI) panelMenu() {
|
||||
if !c.hasPanel() {
|
||||
fmt.Println(" Панели на этой машине нет — это нода")
|
||||
return
|
||||
}
|
||||
c.showPanel()
|
||||
fmt.Println(" 1) Новый случайный путь 2) Свой путь 3) Без пути Enter — назад")
|
||||
switch strings.TrimSpace(c.ask(" › ")) {
|
||||
case "1":
|
||||
c.setPanelPath("new")
|
||||
case "2":
|
||||
if p := strings.TrimSpace(c.ask(" Путь (8–64 символа A–Z a–z 0–9 _ -): ")); p != "" {
|
||||
c.setPanelPath(p)
|
||||
}
|
||||
case "3":
|
||||
if c.confirmYes(" Без пути сканер снова узнает панель по странице логина. Убрать путь?") {
|
||||
c.setPanelPath("off")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user