Отпечаток сервера для сервера лицензий: internal/hwid (machine-id, UUID платформы через /etc/bj/hw-id от установщика, MAC), передаётся при активации и продлении

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-25 00:19:15 +03:00
parent 5ddea22b30
commit d67324e943
3 changed files with 84 additions and 1 deletions
+78
View File
@@ -0,0 +1,78 @@
// Package hwid — отпечаток сервера для сервера лицензий: хэши (sha256) трёх признаков, сами
// значения никуда не передаются. По ним сервер лицензий узнаёт один и тот же сервер у разных
// организаций (защита от «отключил лицензию у одной компании — включил у другой»).
// - machine: /etc/machine-id — меняется при переустановке ОС;
// - board: UUID платформы (BIOS / гипервизор, /sys/class/dmi/id/product_uuid) — у виртуальной
// машины сохраняется при переустановке ОС; читается только root, поэтому установщик кладёт
// его хэш в /etc/bj/hw-id, а bj читает оттуда;
// - mac: MAC-адреса сетевых карт (физических и virtio), отсортированные.
package hwid
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"sort"
"strings"
)
// CacheFile — хэш UUID платформы, записанный установщиком (root) для bj.
const CacheFile = "/etc/bj/hw-id"
func sum(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:])
}
// Machine — хэш /etc/machine-id.
func Machine() string {
b, _ := os.ReadFile("/etc/machine-id")
return sum(string(b))
}
// Board — хэш UUID платформы: из DMI (под root) или из файла, записанного установщиком.
func Board() string {
if b, err := os.ReadFile("/sys/class/dmi/id/product_uuid"); err == nil {
if v := strings.ToLower(strings.TrimSpace(string(b))); v != "" && v != "00000000-0000-0000-0000-000000000000" {
return sum(v)
}
}
b, _ := os.ReadFile(CacheFile)
return strings.TrimSpace(string(b))
}
// MAC — хэш отсортированных MAC-адресов сетевых карт, у которых есть устройство (не lo, не мосты).
func MAC() string {
ents, _ := os.ReadDir("/sys/class/net")
var macs []string
for _, e := range ents {
dir := filepath.Join("/sys/class/net", e.Name())
if _, err := os.Stat(filepath.Join(dir, "device")); err != nil {
continue
}
b, err := os.ReadFile(filepath.Join(dir, "address"))
if a := strings.ToLower(strings.TrimSpace(string(b))); err == nil && a != "" && a != "00:00:00:00:00:00" {
macs = append(macs, a)
}
}
sort.Strings(macs)
return sum(strings.Join(macs, ","))
}
// WriteCache — записать хэш UUID платформы для bj (вызывает установщик под root).
func WriteCache(root string) error {
b := Board()
if b == "" {
return nil
}
p := filepath.Join(root, CacheFile)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
return os.WriteFile(p, []byte(b+"\n"), 0o644)
}
+2
View File
@@ -13,6 +13,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/hwid"
"io"
"log"
"net/http"
@@ -78,6 +79,7 @@ type licenseReply struct {
func licenseCall(ctx context.Context, path string, body map[string]string) (*licenseReply, error) {
host, _ := os.Hostname()
body["hostname"], body["version"], body["machine"] = host, BuildVersion, machineHash()
body["board"], body["mac"] = hwid.Board(), hwid.MAC() // отпечаток сервера: хэши, без самих значений
b, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseServer+path, bytes.NewReader(b))
if err != nil {