1.5.0 update

This commit is contained in:
KingKDot
2026-03-20 22:38:17 -05:00
parent 265321dce2
commit e8ad1e3649
30 changed files with 2259 additions and 79 deletions
@@ -0,0 +1,52 @@
package config
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"log"
"golang.org/x/crypto/hkdf"
)
type Identity struct {
PublicKey ed25519.PublicKey
PrivateKey ed25519.PrivateKey
Fingerprint string // hex SHA256 of public key
}
func DeriveIdentity() Identity {
hwid := deriveHWID()
machineID := platformMachineID()
if machineID == "" {
log.Printf("[identity] WARNING: no OS-specific machine ID available, using HWID-only derivation")
machineID = hwid
}
hkdfReader := hkdf.New(sha256.New, []byte(machineID), []byte(hwid), []byte("overlord-identity"))
seed := make([]byte, ed25519.SeedSize)
if _, err := io.ReadFull(hkdfReader, seed); err != nil {
log.Fatalf("[identity] HKDF derivation failed: %v", err)
}
privateKey := ed25519.NewKeyFromSeed(seed)
publicKey := privateKey.Public().(ed25519.PublicKey)
fp := sha256.Sum256(publicKey)
return Identity{
PublicKey: publicKey,
PrivateKey: privateKey,
Fingerprint: hex.EncodeToString(fp[:]),
}
}
func (id *Identity) Sign(data []byte) []byte {
return ed25519.Sign(id.PrivateKey, data)
}
func (id *Identity) PublicKeyBase64() string {
return base64.StdEncoding.EncodeToString(id.PublicKey)
}
@@ -0,0 +1,7 @@
//go:build android
package config
func platformMachineID() string {
return ""
}
@@ -0,0 +1,27 @@
//go:build darwin
package config
import (
"log"
"os/exec"
"strings"
)
func platformMachineID() string {
out, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
if err != nil {
log.Printf("[identity] WARNING: failed to run ioreg: %v", err)
return ""
}
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, "IOPlatformUUID") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
return strings.Trim(strings.TrimSpace(parts[1]), `"`)
}
}
}
log.Printf("[identity] WARNING: IOPlatformUUID not found in ioreg output")
return ""
}
@@ -0,0 +1,18 @@
//go:build linux && !android
package config
import (
"log"
"os"
"strings"
)
func platformMachineID() string {
data, err := os.ReadFile("/etc/machine-id")
if err != nil {
log.Printf("[identity] WARNING: failed to read /etc/machine-id: %v", err)
return ""
}
return strings.TrimSpace(string(data))
}
@@ -0,0 +1,25 @@
//go:build windows
package config
import (
"log"
"golang.org/x/sys/windows/registry"
)
func platformMachineID() string {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, registry.QUERY_VALUE)
if err != nil {
log.Printf("[identity] WARNING: failed to open Cryptography registry key: %v", err)
return ""
}
defer k.Close()
val, _, err := k.GetStringValue("MachineGuid")
if err != nil {
log.Printf("[identity] WARNING: failed to read MachineGuid: %v", err)
return ""
}
return val
}
+123
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"log"
@@ -134,6 +135,12 @@ func runClient(cfg config.Config) {
sleepFor = reconnectDelay()
log.Printf("reconnect: immediate retry in %s", sleepFor)
}
if sessionErr != nil {
if d := enrollmentRetryDelay(sessionErr); d > 0 {
sleepFor = d
log.Printf("purgatory: retry in %s", sleepFor)
}
}
time.Sleep(sleepFor)
}
}
@@ -304,6 +311,9 @@ func shouldRetryImmediately(err error) bool {
}
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) {
if closeErr.Code == 4001 || closeErr.Code == 4002 || closeErr.Code == 4003 {
return false
}
if closeErr.Code == websocket.StatusNormalClosure || closeErr.Code == websocket.StatusGoingAway {
return true
}
@@ -321,6 +331,43 @@ func shouldRetryImmediately(err error) bool {
return false
}
func enrollmentRetryDelay(err error) time.Duration {
if err == nil {
return 0
}
msg := err.Error()
if strings.Contains(msg, "purgatory: status=pending") {
return getEnrollmentRetryInterval()
}
if strings.Contains(msg, "purgatory: status=denied") {
return 5 * time.Minute
}
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) {
switch closeErr.Code {
case 4001: // pending
return getEnrollmentRetryInterval()
case 4002: // invalid signature
return 60 * time.Second
case 4003: // denied
return 5 * time.Minute
}
}
return 0
}
func getEnrollmentRetryInterval() time.Duration {
raw := strings.TrimSpace(os.Getenv("OVERLORD_ENROLLMENT_RETRY_MS"))
if raw != "" {
if ms, err := strconv.Atoi(raw); err == nil && ms > 0 {
return time.Duration(ms) * time.Millisecond
}
}
return 30 * time.Second
}
func getPingInterval() time.Duration {
raw := strings.TrimSpace(os.Getenv("OVERLORD_PING_INTERVAL_MS"))
if raw == "" {
@@ -350,7 +397,50 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
defer cancel()
defer conn.Close(websocket.StatusNormalClosure, "bye")
identity := config.DeriveIdentity()
log.Printf("[purgatory] identity fingerprint=%s", identity.Fingerprint)
challengeCtx, challengeCancel := context.WithTimeout(ctx, 30*time.Second)
defer challengeCancel()
_, challengeData, err := conn.Read(challengeCtx)
if err != nil {
return fmt.Errorf("purgatory: failed to read challenge: %w", err)
}
challengeEnvelope, err := wire.DecodeEnvelope(challengeData)
if err != nil {
return fmt.Errorf("purgatory: failed to decode challenge: %w", err)
}
safeWriter := wire.NewSafeWriter(conn)
msgType, _ := challengeEnvelope["type"].(string)
var publicKeyB64, signatureB64 string
switch msgType {
case "enrollment_challenge":
nonceB64, _ := challengeEnvelope["nonce"].(string)
if nonceB64 == "" {
return fmt.Errorf("purgatory: empty nonce in challenge")
}
nonceBytes, decErr := base64.StdEncoding.DecodeString(nonceB64)
if decErr != nil {
return fmt.Errorf("purgatory: invalid nonce base64: %w", decErr)
}
sig := identity.Sign(nonceBytes)
publicKeyB64 = identity.PublicKeyBase64()
signatureB64 = base64.StdEncoding.EncodeToString(sig)
log.Printf("[purgatory] signed challenge nonce (%d bytes)", len(nonceBytes))
case "hello_ack":
log.Printf("[purgatory] legacy server (no challenge), proceeding")
publicKeyB64 = ""
signatureB64 = ""
default:
return fmt.Errorf("purgatory: unexpected first message type: %s", msgType)
}
env := &rt.Env{Conn: safeWriter, Cfg: cfg, Cancel: cancel, SelectedDisplay: handlers.GetPersistedDisplay()}
env.SetLastPong(time.Now().UnixMilli())
env.Console = rt.NewConsoleHub(env)
@@ -389,12 +479,45 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
MonitorInfo: toWireMonitorInfo(capture.MonitorInfos()),
Country: cfg.Country,
BuildTag: cfg.BuildTag,
PublicKey: publicKeyB64,
Signature: signatureB64,
}
if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil {
return fmt.Errorf("send hello: %w", err)
}
if msgType == "enrollment_challenge" {
ackCtx, ackCancel := context.WithTimeout(ctx, 30*time.Second)
defer ackCancel()
for {
_, ackData, err := conn.Read(ackCtx)
if err != nil {
return fmt.Errorf("purgatory: failed to read response: %w", err)
}
ackEnvelope, err := wire.DecodeEnvelope(ackData)
if err != nil {
return fmt.Errorf("purgatory: failed to decode response: %w", err)
}
ackType, _ := ackEnvelope["type"].(string)
switch ackType {
case "hello_ack":
log.Printf("[purgatory] approved, proceeding with session")
case "enrollment_status":
status, _ := ackEnvelope["status"].(string)
log.Printf("[purgatory] server returned status=%s", status)
return fmt.Errorf("purgatory: status=%s", status)
case "ping", "pong":
continue
default:
return fmt.Errorf("purgatory: unexpected response type: %s", ackType)
}
break
}
}
if err := wire.WriteMsg(ctx, env.Conn, wire.Ping{Type: "ping", TS: time.Now().UnixMilli()}); err != nil {
log.Printf("ping: failed to send initial ping: %v", err)
cancel()
@@ -13,6 +13,18 @@ type Hello struct {
MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"`
Country string `msgpack:"country,omitempty"`
BuildTag string `msgpack:"buildTag,omitempty"`
PublicKey string `msgpack:"publicKey,omitempty"`
Signature string `msgpack:"signature,omitempty"`
}
type EnrollmentChallenge struct {
Type string `msgpack:"type"`
Nonce string `msgpack:"nonce"`
}
type EnrollmentStatus struct {
Type string `msgpack:"type"`
Status string `msgpack:"status"`
}
type MonitorInfo struct {
+3 -2
View File
@@ -1,6 +1,6 @@
module overlord-client
go 1.24
go 1.25.0
require (
github.com/creack/pty v1.1.21
@@ -14,7 +14,8 @@ require (
require (
github.com/Kirizu-Official/windows-camera-go v0.2.0
github.com/gen2brain/malgo v0.11.24
golang.org/x/sys v0.24.0
golang.org/x/crypto v0.49.0
golang.org/x/sys v0.42.0
)
require (
+4 -2
View File
@@ -32,9 +32,11 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=