Stealth sen:// port, random cert names, external Prometheus card

The subscription port answered every request with signed JSON: one probe
told a scanner what it had found, and the public key from any sen:// link
pinned the exact server. It now speaks the protocol only to those who prove
a secret — peek and register with the link's master secret, everything else
with a valid device signature — and gives Go's plain "404 page not found"
to the rest. A deleted device still gets its 410, but only when the request
is signed with its key, kept in the new revoked_devices table. The migration
freeze answers only proven clients too.

Self-signed certs for the subscription (SUB_TLS=on) and the node hub got
fixed names (sen, awg-node); new ones get a random host-like name. Clients
and nodes pin the key, so existing certs keep working.

The panel could already issue a metrics-only token, but nothing in the UI
used it. The Monitor tab gets an "External Prometheus" card: issue, reissue
and revoke the token, with a ready prometheus.yml fragment whose address is
taken from the page, so it carries the secret path or the nginx subfolder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
This commit is contained in:
Claude
2026-10-28 11:52:55 +00:00
parent 619af21085
commit a3d4664214
8 changed files with 331 additions and 53 deletions
+15 -2
View File
@@ -153,14 +153,15 @@ func newNodes(a *App) (*Nodes, error) {
}
return filepath.Join(a.baseDir, name)
}
// TLS: самоподписанный сертификат, pin которого зашит в join-строки. Живёт столько же,
// TLS: самоподписанный сертификат, pin которого зашит в join-строки. Имя в нём случайное:
// одинаковое у всех серверов было бы сигнатурой для сканера (pin — по ключу, не по имени). Живёт столько же,
// сколько core: пересоздать = переподключить все ноды.
if n.port != 0 {
keyF, crtF := file("NODE_TLS_KEY_FILE", "node_tls.key"), file("NODE_TLS_CERT_FILE", "node_tls.crt")
if err := func() error {
if !exists(keyF) && !exists(crtF) {
cmd := exec.Command("openssl", "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", "-nodes",
"-keyout", keyF, "-out", crtF, "-days", "3650", "-subj", "/CN=awg-node")
"-keyout", keyF, "-out", crtF, "-days", "3650", "-subj", "/CN="+randomCN())
if err := cmd.Run(); err != nil {
return err
}
@@ -927,3 +928,15 @@ func (n *Nodes) router() *Router {
})
return rt
}
// randomCN — имя для самоподписанного сертификата: похожее на обычный хост и своё у каждого
// сервера.
func randomCN() string {
const abc = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 10)
rand.Read(b)
for i := range b {
b[i] = abc[int(b[i])%len(abc)]
}
return string(b) + ".net"
}